From 1e50d8d5c2ea044885c4f98198b96a773f3e8c97 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 23 May 2015 17:33:25 +0200 Subject: [PATCH] Implement toString() for synthesized annotations Issue: SPR-13064 --- .../core/annotation/AnnotationUtils.java | 2 +- ...ynthesizedAnnotationInvocationHandler.java | 45 +++++++++-- .../core/annotation/AnnotationUtilsTests.java | 79 ++++++++++++++++--- 3 files changed, 110 insertions(+), 16 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 50ebcd032e2..1df0de3a6f6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -827,7 +827,6 @@ public abstract class AnnotationUtils { AnnotationAttributes attrs = new AnnotationAttributes(annotationType); for (Method method : getAttributeMethods(annotationType)) { try { - ReflectionUtils.makeAccessible(method); Object value = method.invoke(annotation); Object defaultValue = method.getDefaultValue(); @@ -1274,6 +1273,7 @@ public abstract class AnnotationUtils { List methods = new ArrayList(); for (Method method : annotationType.getDeclaredMethods()) { if ((method.getParameterTypes().length == 0) && (method.getReturnType() != void.class)) { + ReflectionUtils.makeAccessible(method); methods.add(method); } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java index e6dfa647487..2491255cc79 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java @@ -20,10 +20,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; import java.util.Map; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * {@link InvocationHandler} for an {@link Annotation} that Spring has @@ -38,6 +41,7 @@ import org.springframework.util.ReflectionUtils; * * @author Sam Brannen * @since 4.2 + * @see Annotation * @see AliasFor * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) */ @@ -62,10 +66,16 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String attributeName = method.getName(); + String methodName = method.getName(); + int parameterCount = method.getParameterCount(); + + if ("toString".equals(methodName) && (parameterCount == 0)) { + return toString(proxy); + } + Class returnType = method.getReturnType(); boolean nestedAnnotation = (Annotation[].class.isAssignableFrom(returnType) || Annotation.class.isAssignableFrom(returnType)); - String aliasedAttributeName = aliasMap.get(attributeName); + String aliasedAttributeName = aliasMap.get(methodName); boolean aliasPresent = (aliasedAttributeName != null); ReflectionUtils.makeAccessible(method); @@ -83,14 +93,14 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { } catch (NoSuchMethodException e) { String msg = String.format("In annotation [%s], attribute [%s] is declared as an @AliasFor [%s], " - + "but attribute [%s] does not exist.", this.annotationType.getName(), attributeName, + + "but attribute [%s] does not exist.", this.annotationType.getName(), methodName, aliasedAttributeName, aliasedAttributeName); throw new AnnotationConfigurationException(msg); } ReflectionUtils.makeAccessible(aliasedMethod); Object aliasedValue = ReflectionUtils.invokeMethod(aliasedMethod, this.annotation, args); - Object defaultValue = AnnotationUtils.getDefaultValue(this.annotation, attributeName); + Object defaultValue = AnnotationUtils.getDefaultValue(this.annotation, methodName); if (!ObjectUtils.nullSafeEquals(value, aliasedValue) && !ObjectUtils.nullSafeEquals(value, defaultValue) && !ObjectUtils.nullSafeEquals(aliasedValue, defaultValue)) { @@ -98,7 +108,7 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { String msg = String.format( "In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] are " + "declared with values of [%s] and [%s], but only one declaration is permitted.", - this.annotationType.getName(), elementName, attributeName, aliasedAttributeName, + this.annotationType.getName(), elementName, methodName, aliasedAttributeName, ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue)); throw new AnnotationConfigurationException(msg); } @@ -124,4 +134,29 @@ class SynthesizedAnnotationInvocationHandler implements InvocationHandler { return value; } + private String toString(Object proxy) { + StringBuilder sb = new StringBuilder("@").append(annotationType.getName()).append("("); + + List attributeMethods = AnnotationUtils.getAttributeMethods(this.annotationType); + Iterator iterator = attributeMethods.iterator(); + while (iterator.hasNext()) { + Method attributeMethod = iterator.next(); + sb.append(attributeMethod.getName()); + sb.append('='); + sb.append(valueToString(ReflectionUtils.invokeMethod(attributeMethod, proxy))); + sb.append(iterator.hasNext() ? ", " : ""); + } + + return sb.append(")").toString(); + } + + private String valueToString(Object value) { + if (value instanceof Object[]) { + return "[" + StringUtils.arrayToDelimitedString((Object[]) value, ", ") + "]"; + } + + // else + return String.valueOf(value); + } + } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index 4fe618a728d..79f8e6e5cf0 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -413,7 +413,7 @@ public class AnnotationUtilsTests { assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); assertEquals("path attribute: ", "/test", attributes.getString("path")); - method = WebController.class.getMethod("handleMappedWithPathValueAndAttributes"); + method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes"); webMapping = method.getAnnotation(WebMapping.class); exception.expect(AnnotationConfigurationException.class); exception.expectMessage(containsString("attribute [value] and its alias [path]")); @@ -600,14 +600,60 @@ public class AnnotationUtilsTests { Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); WebMapping webMapping = method.getAnnotation(WebMapping.class); assertNotNull(webMapping); - WebMapping synthesizedWebMapping = synthesizeAnnotation(webMapping); - assertNotSame(webMapping, synthesizedWebMapping); - assertThat(synthesizedWebMapping, instanceOf(SynthesizedAnnotation.class)); - assertNotNull(synthesizedWebMapping); - assertEquals("name attribute: ", "foo", synthesizedWebMapping.name()); - assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping.path()); - assertEquals("actual value attribute: ", "/test", synthesizedWebMapping.value()); + WebMapping synthesizedWebMapping1 = synthesizeAnnotation(webMapping); + assertNotNull(synthesizedWebMapping1); + assertNotSame(webMapping, synthesizedWebMapping1); + assertThat(synthesizedWebMapping1, instanceOf(SynthesizedAnnotation.class)); + + assertEquals("name attribute: ", "foo", synthesizedWebMapping1.name()); + assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping1.path()); + assertEquals("actual value attribute: ", "/test", synthesizedWebMapping1.value()); + + WebMapping synthesizedWebMapping2 = synthesizeAnnotation(webMapping); + assertNotNull(synthesizedWebMapping2); + assertNotSame(webMapping, synthesizedWebMapping2); + assertThat(synthesizedWebMapping2, instanceOf(SynthesizedAnnotation.class)); + + assertEquals("name attribute: ", "foo", synthesizedWebMapping2.name()); + assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping2.path()); + assertEquals("actual value attribute: ", "/test", synthesizedWebMapping2.value()); + } + + @Test + public void toStringForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + WebMapping webMappingWithAliases = methodWithPath.getAnnotation(WebMapping.class); + assertNotNull(webMappingWithAliases); + + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + WebMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(WebMapping.class); + assertNotNull(webMappingWithPathAndValue); + + WebMapping synthesizedWebMapping1 = synthesizeAnnotation(webMappingWithAliases); + assertNotNull(synthesizedWebMapping1); + WebMapping synthesizedWebMapping2 = synthesizeAnnotation(webMappingWithAliases); + assertNotNull(synthesizedWebMapping2); + + assertThat(webMappingWithAliases.toString(), is(not(synthesizedWebMapping1.toString()))); + + // The unsynthesized annotation for handleMappedWithSamePathAndValueAttributes() + // should produce the same toString() results as synthesized annotations for + // handleMappedWithPathAttribute() + assertToStringForWebMappingWithPathAndValue(webMappingWithPathAndValue); + assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping1); + assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping2); + } + + private void assertToStringForWebMappingWithPathAndValue(WebMapping webMapping) { + String string = webMapping.toString(); + assertThat(string, startsWith("@" + WebMapping.class.getName() + "(")); + assertThat(string, containsString("value=/test")); + assertThat(string, containsString("path=/test")); + assertThat(string, containsString("name=bar")); + assertThat(string, containsString("method=")); + assertThat(string, either(containsString("[GET, POST]")).or(containsString("[POST, GET]"))); + assertThat(string, endsWith(")")); } /** @@ -942,6 +988,10 @@ public class AnnotationUtilsTests { void foo(); } + enum RequestMethod { + GET, POST + } + /** * Mock of {@code org.springframework.web.bind.annotation.RequestMapping}. */ @@ -955,6 +1005,8 @@ public class AnnotationUtilsTests { @AliasFor(attribute = "value") String path() default ""; + + RequestMethod[] method() default {}; } @Component("webController") @@ -964,12 +1016,19 @@ public class AnnotationUtilsTests { public void handleMappedWithValueAttribute() { } - @WebMapping(path = "/test", name = "bar") + @WebMapping(path = "/test", name = "bar", method = { RequestMethod.GET, RequestMethod.POST }) public void handleMappedWithPathAttribute() { } + /** + * mapping is logically "equal" to handleMappedWithPathAttribute(). + */ + @WebMapping(value = "/test", path = "/test", name = "bar", method = { RequestMethod.GET, RequestMethod.POST }) + public void handleMappedWithSamePathAndValueAttributes() { + } + @WebMapping(value = "/enigma", path = "/test", name = "baz") - public void handleMappedWithPathValueAndAttributes() { + public void handleMappedWithDifferentPathAndValueAttributes() { } }