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 { }