Implement toString() for synthesized annotations
Issue: SPR-13064
This commit is contained in:
@@ -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<Method> methods = new ArrayList<Method>();
|
||||
for (Method method : annotationType.getDeclaredMethods()) {
|
||||
if ((method.getParameterTypes().length == 0) && (method.getReturnType() != void.class)) {
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
methods.add(method);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Method> attributeMethods = AnnotationUtils.getAttributeMethods(this.annotationType);
|
||||
Iterator<Method> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user