Skip to content

Commit 7139a87

Browse files
committed
Ensure toString() for synthesized annotations is source code compatible
Since the introduction of synthesized annotation support in Spring Framework 4.2 (a.k.a., merged annotations), the toString() implementation attempted to align with the formatting used by the JDK itself. However, Class annotation attributes were formatted using Class#getName in Spring; whereas, the JDK used Class#toString up until JDK 9. In addition, JDK 9 introduced new formatting for toString() for annotations, apparently intended to align with the syntax used in the source code declaration of the annotation. However, JDK 9+ formats enum annotation attributes using Enum#toString instead of Enum#name, which can lead to issues if toString() is overridden in an enum. This commit updates the formatting used for synthesized annotations by ensuring that toString() generates a string that is compatible with the syntax of the originating source code, going beyond the changes made in JDK 9 by using Enum#name instead of Enum#toString. Closes gh-28015
1 parent 669b05d commit 7139a87

File tree

2 files changed

+50
-9
lines changed

2 files changed

+50
-9
lines changed

spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -195,18 +195,24 @@ private String annotationToString() {
195195
}
196196

197197
private String toString(Object value) {
198+
if (value instanceof String) {
199+
return '"' + value.toString() + '"';
200+
}
201+
if (value instanceof Enum) {
202+
return ((Enum<?>) value).name();
203+
}
198204
if (value instanceof Class) {
199-
return ((Class<?>) value).getName();
205+
return ((Class<?>) value).getName() + ".class";
200206
}
201207
if (value.getClass().isArray()) {
202-
StringBuilder builder = new StringBuilder("[");
208+
StringBuilder builder = new StringBuilder("{");
203209
for (int i = 0; i < Array.getLength(value); i++) {
204210
if (i > 0) {
205211
builder.append(", ");
206212
}
207213
builder.append(toString(Array.get(value, i)));
208214
}
209-
builder.append(']');
215+
builder.append('}');
210216
return builder.toString();
211217
}
212218
return String.valueOf(value);

spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
import javax.annotation.Resource;
3939

4040
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.condition.JRE;
4142

4243
import org.springframework.core.Ordered;
4344
import org.springframework.core.annotation.MergedAnnotation.Adapt;
@@ -1861,20 +1862,41 @@ void toStringForSynthesizedAnnotations() throws Exception {
18611862
Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute");
18621863
RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class);
18631864
assertThat(webMappingWithAliases).isNotNull();
1865+
18641866
Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes");
18651867
RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class);
18661868
assertThat(methodWithPathAndValue).isNotNull();
1869+
18671870
RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize();
18681871
RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize();
1872+
18691873
assertThat(webMappingWithAliases.toString()).isNotEqualTo(synthesizedWebMapping1.toString());
1874+
1875+
if (JRE.currentVersion().ordinal() > JRE.JAVA_8.ordinal()) {
1876+
// The unsynthesized annotation for handleMappedWithSamePathAndValueAttributes()
1877+
// should produce the same toString() results as synthesized annotations for
1878+
// handleMappedWithPathAttribute() on Java 9 or higher
1879+
assertToStringForWebMappingWithPathAndValue(webMappingWithPathAndValue);
1880+
}
18701881
assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping1);
18711882
assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping2);
18721883
}
18731884

18741885
private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) {
1875-
String prefix = "@" + RequestMapping.class.getName() + "(";
1876-
assertThat(webMapping.toString()).startsWith(prefix).contains("value=[/test]",
1877-
"path=[/test]", "name=bar", "method=", "[GET, POST]").endsWith(")");
1886+
String string = webMapping.toString();
1887+
1888+
// Formatting common to Spring and JDK 9+
1889+
assertThat(string)
1890+
.startsWith("@" + RequestMapping.class.getName() + "(")
1891+
.contains("value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", "clazz=java.lang.Object.class")
1892+
.endsWith(")");
1893+
1894+
if (webMapping instanceof SynthesizedAnnotation) {
1895+
assertThat(string).as("Spring uses Enum#name()").contains("method={GET, POST}");
1896+
}
1897+
else {
1898+
assertThat(string).as("JDK uses Enum#toString()").contains("method={method: get, method: post}");
1899+
}
18781900
}
18791901

18801902
@Test
@@ -2941,7 +2963,17 @@ static class SubSubMyRepeatableWithAdditionalLocalDeclarationsClass
29412963
}
29422964

29432965
enum RequestMethod {
2944-
GET, POST
2966+
GET,
2967+
2968+
POST;
2969+
2970+
/**
2971+
* custom override to verify annotation toString() implementations.
2972+
*/
2973+
@Override
2974+
public String toString() {
2975+
return "method: " + name().toLowerCase();
2976+
}
29452977
}
29462978

29472979
@Retention(RetentionPolicy.RUNTIME)
@@ -2956,6 +2988,9 @@ enum RequestMethod {
29562988
String[] path() default "";
29572989

29582990
RequestMethod[] method() default {};
2991+
2992+
// clazz is only used for testing annotation toString() implementations
2993+
Class<?> clazz() default Object.class;
29592994
}
29602995

29612996
@Retention(RetentionPolicy.RUNTIME)

0 commit comments

Comments
 (0)