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 0e977696c65..24c21acf6af 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
@@ -137,14 +137,20 @@ final class SynthesizedMergedAnnotationInvocationHandler i
String string = this.string;
if (string == null) {
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) {
- builder.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))));
+ }
+ else {
+ for (int i = 0; i < this.attributes.size(); i++) {
+ Method attribute = this.attributes.get(i);
+ if (i > 0) {
+ builder.append(", ");
+ }
+ builder.append(attribute.getName());
+ builder.append('=');
+ builder.append(toString(getAttributeValue(attribute)));
}
- builder.append(attribute.getName());
- builder.append('=');
- builder.append(toString(getAttributeValue(attribute)));
}
builder.append(')');
string = builder.toString();
@@ -187,7 +193,7 @@ final class SynthesizedMergedAnnotationInvocationHandler i
return '\'' + value.toString() + '\'';
}
if (type == Byte.class) {
- return String.format("(byte) 0x%02X", value);
+ return String.format("(byte)0x%02x", value);
}
if (type == Long.class) {
return Long.toString((Long) value) + 'L';
@@ -196,7 +202,7 @@ final class SynthesizedMergedAnnotationInvocationHandler i
return Float.toString((Float) value) + 'f';
}
if (type == Double.class) {
- return Double.toString((Double) value) + 'd';
+ return Double.toString((Double) value);
}
if (value instanceof Enum> e) {
return e.name();
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 480f5ad2d09..276fb7167c7 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
@@ -2011,9 +2011,9 @@ class MergedAnnotationsTests {
// The unsynthesized annotation for handleMappedWithSamePathAndValueAttributes()
// should produce almost the same toString() results as synthesized annotations for
- // handleMappedWithPathAttribute() on Java 9 or higher; however, due to multiple changes
- // in the JDK's toString() implementation for annotations in JDK 9, 14, and 19,
- // we do not test the JDK implementation.
+ // handleMappedWithPathAttribute(); however, due to multiple changes in the JDK's
+ // toString() implementation for annotations in JDK 19 and higher, we do not test
+ // the JDK implementation.
// assertToStringForWebMappingWithPathAndValue(webMappingWithPathAndValue);
assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping1);
@@ -2021,7 +2021,7 @@ class MergedAnnotationsTests {
}
private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) {
- assertThat(webMapping.toString())
+ assertThat(webMapping).asString()
.startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(")
.contains(
// Strings
@@ -2034,7 +2034,7 @@ class MergedAnnotationsTests {
"clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class",
"classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}",
// Bytes
- "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}",
+ "byteValue=(byte)0xff", "bytes={(byte)0xff}",
// Shorts
"shortValue=9876", "shorts={9876}",
// Longs
@@ -2042,11 +2042,24 @@ class MergedAnnotationsTests {
// Floats
"floatValue=3.14f", "floats={3.14f}",
// Doubles
- "doubleValue=99.999d", "doubles={99.999d}"
+ "doubleValue=99.999", "doubles={99.999}"
)
.endsWith(")");
}
+ @Test // gh-36417
+ void toStringForSynthesizedAnnotationsWithSingleValueAttributes() {
+ MyRepeatable myRepeatable = MergedAnnotations.from(SingleMyRepeatableClass.class)
+ .get(MyRepeatable.class).synthesize();
+ assertThat(myRepeatable).asString()
+ .isEqualTo("@%s(\"meta\")", MyRepeatable.class.getCanonicalName());
+
+ ValueAttribute valueAttribute = MergedAnnotations.from(ValueAttributeMetaMetaClass.class)
+ .get(ValueAttribute.class).synthesize();
+ assertThat(valueAttribute).asString()
+ .isEqualTo("@%s({\"FromValueAttributeMeta\"})", ValueAttribute.class.getCanonicalName());
+ }
+
@Test
void equalsForSynthesizedAnnotations() throws Exception {
Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute");
@@ -3085,6 +3098,16 @@ class MergedAnnotationsTests {
static class MyRepeatableClass {
}
+ @Retention(RetentionPolicy.RUNTIME)
+ @Inherited
+ @MyRepeatable("meta")
+ @interface SingleMyRepeatable {
+ }
+
+ @SingleMyRepeatable
+ static class SingleMyRepeatableClass {
+ }
+
static class SubMyRepeatableClass extends MyRepeatableClass {
}