diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java
index 3d0ef51ab5c..992b1a07fa7 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java
@@ -177,7 +177,7 @@ final class SynthesizedMergedAnnotationInvocationHandler i
private String annotationToString() {
String string = this.string;
if (string == null) {
- StringBuilder builder = new StringBuilder("@").append(this.type.getName()).append('(');
+ StringBuilder builder = new StringBuilder("@").append(getName(this.type)).append('(');
for (int i = 0; i < this.attributes.size(); i++) {
Method attribute = this.attributes.get(i);
if (i > 0) {
@@ -194,15 +194,43 @@ final class SynthesizedMergedAnnotationInvocationHandler i
return string;
}
+ /**
+ * This method currently does not address the following issues which we may
+ * choose to address at a later point in time.
+ *
+ *
+ * - non-ASCII, non-visible, and non-printable characters within a character
+ * or String literal are not escaped.
+ * - formatting for float and double values does not take into account whether
+ * a value is not a number (NaN) or infinite.
+ *
+ * @param value the attribute value to format
+ * @return the formatted string representation
+ */
private String toString(Object value) {
if (value instanceof String str) {
return '"' + str + '"';
}
+ if (value instanceof Character) {
+ return '\'' + value.toString() + '\'';
+ }
+ if (value instanceof Byte) {
+ return String.format("(byte) 0x%02X", value);
+ }
+ if (value instanceof Long longValue) {
+ return Long.toString(longValue) + 'L';
+ }
+ if (value instanceof Float floatValue) {
+ return Float.toString(floatValue) + 'f';
+ }
+ if (value instanceof Double doubleValue) {
+ return Double.toString(doubleValue) + 'd';
+ }
if (value instanceof Enum> e) {
return e.name();
}
if (value instanceof Class> clazz) {
- return clazz.getName() + ".class";
+ return getName(clazz) + ".class";
}
if (value.getClass().isArray()) {
StringBuilder builder = new StringBuilder("{");
@@ -277,6 +305,11 @@ final class SynthesizedMergedAnnotationInvocationHandler i
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
}
+ private static String getName(Class> clazz) {
+ String canonicalName = clazz.getCanonicalName();
+ return (canonicalName != null ? canonicalName : clazz.getName());
+ }
+
private static boolean isVisible(ClassLoader classLoader, Class> interfaceClass) {
if (classLoader == interfaceClass.getClassLoader()) {
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
index 435a99a5adc..3a1151e5f62 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
@@ -37,6 +37,7 @@ import java.util.stream.Stream;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.JRE;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
@@ -1883,15 +1884,44 @@ class MergedAnnotationsTests {
// Formatting common to Spring and JDK 9+
assertThat(string)
- .startsWith("@" + RequestMapping.class.getName() + "(")
- .contains("value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", "clazz=java.lang.Object.class")
+ .contains("value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", "ch='X'", "chars={'X'}")
.endsWith(")");
if (webMapping instanceof SynthesizedAnnotation) {
- assertThat(string).as("Spring uses Enum#name()").contains("method={GET, POST}");
+ assertThat(string).as("Spring formatting")
+ .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(")
+ .contains("method={GET, POST}",
+ "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class",
+ "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}",
+ "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}",
+ "shortValue=9876", "shorts={9876}",
+ "longValue=42L", "longs={42L}",
+ "floatValue=3.14f", "floats={3.14f}",
+ "doubleValue=99.999d", "doubles={99.999d}"
+ );
}
else {
- assertThat(string).as("JDK uses Enum#toString()").contains("method={method: get, method: post}");
+ assertThat(string).as("JDK 9-18 formatting")
+ .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests$RequestMapping(")
+ .contains("method={method: get, method: post}",
+ "clazz=org.springframework.core.annotation.MergedAnnotationsTests$RequestMethod.class",
+ "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests$RequestMethod[].class}",
+ "shortValue=9876", "shorts={9876}",
+ "floatValue=3.14f", "floats={3.14f}",
+ "doubleValue=99.999", "doubles={99.999}"
+ );
+ if (JRE.currentVersion().ordinal() < JRE.JAVA_14.ordinal()) {
+ assertThat(string).as("JDK 9-13 formatting")
+ .contains("longValue=42", "longs={42}",
+ "byteValue=-1", "bytes={-1}"
+ );
+ }
+ else {
+ assertThat(string).as("JDK 14+ formatting")
+ .contains("longValue=42L", "longs={42L}",
+ "byteValue=(byte)0xff", "bytes={(byte)0xff}"
+ );
+ }
}
}
@@ -2985,8 +3015,29 @@ class MergedAnnotationsTests {
RequestMethod[] method() default {};
- // clazz is only used for testing annotation toString() implementations
- Class> clazz() default Object.class;
+ // ---------------------------------------------------------------------
+ // All remaining attributes declare default values that are used solely
+ // for the purpose of testing the toString() implementations for annotations.
+ Class> clazz() default RequestMethod.class;
+ Class>[] classes() default {int[][].class, RequestMethod[].class};
+
+ char ch() default 'X';
+ char[] chars() default {'X'};
+
+ byte byteValue() default (byte) 0xFF;
+ byte[] bytes() default {(byte) 0xFF};
+
+ short shortValue() default 9876;
+ short[] shorts() default {9876};
+
+ long longValue() default 42L;
+ long[] longs() default {42L};
+
+ float floatValue() default 3.14F;
+ float[] floats() default {3.14F};
+
+ double doubleValue() default 99.999D;
+ double[] doubles() default {99.999D};
}
@Retention(RetentionPolicy.RUNTIME)
diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java
index 7ed75786f8f..5c409d88044 100644
--- a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -72,8 +72,8 @@ class BootstrapUtilsTests {
assertThatIllegalStateException().isThrownBy(() ->
resolveTestContextBootstrapper(bootstrapContext))
.withMessageContaining("Configuration error: found multiple declarations of @BootstrapWith")
- .withMessageContaining(FooBootstrapper.class.getName())
- .withMessageContaining(BarBootstrapper.class.getName());
+ .withMessageContaining(FooBootstrapper.class.getCanonicalName())
+ .withMessageContaining(BarBootstrapper.class.getCanonicalName());
}
@Test