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 3d0ef51ab5c..992b1a07fa7 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 @@ -177,7 +177,7 @@ final class SynthesizedMergedAnnotationInvocationHandler i private String annotationToString() { String string = this.string; if (string == null) { - StringBuilder builder = new StringBuilder("@").append(this.type.getName()).append('('); + 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) { @@ -194,15 +194,43 @@ final class SynthesizedMergedAnnotationInvocationHandler i return string; } + /** + * This method currently does not address the following issues which we may + * choose to address at a later point in time. + * + * + * @param value the attribute value to format + * @return the formatted string representation + */ private String toString(Object value) { if (value instanceof String str) { return '"' + str + '"'; } + if (value instanceof Character) { + return '\'' + value.toString() + '\''; + } + if (value instanceof Byte) { + return String.format("(byte) 0x%02X", value); + } + if (value instanceof Long longValue) { + return Long.toString(longValue) + 'L'; + } + if (value instanceof Float floatValue) { + return Float.toString(floatValue) + 'f'; + } + if (value instanceof Double doubleValue) { + return Double.toString(doubleValue) + 'd'; + } if (value instanceof Enum e) { return e.name(); } if (value instanceof Class clazz) { - return clazz.getName() + ".class"; + return getName(clazz) + ".class"; } if (value.getClass().isArray()) { StringBuilder builder = new StringBuilder("{"); @@ -277,6 +305,11 @@ final class SynthesizedMergedAnnotationInvocationHandler i return (A) Proxy.newProxyInstance(classLoader, interfaces, handler); } + private static String getName(Class clazz) { + String canonicalName = clazz.getCanonicalName(); + return (canonicalName != null ? canonicalName : clazz.getName()); + } + private static boolean isVisible(ClassLoader classLoader, Class interfaceClass) { if (classLoader == interfaceClass.getClassLoader()) { 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 435a99a5adc..3a1151e5f62 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 @@ -37,6 +37,7 @@ import java.util.stream.Stream; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.JRE; import org.springframework.core.Ordered; import org.springframework.core.annotation.MergedAnnotation.Adapt; @@ -1883,15 +1884,44 @@ class MergedAnnotationsTests { // Formatting common to Spring and JDK 9+ assertThat(string) - .startsWith("@" + RequestMapping.class.getName() + "(") - .contains("value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", "clazz=java.lang.Object.class") + .contains("value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", "ch='X'", "chars={'X'}") .endsWith(")"); if (webMapping instanceof SynthesizedAnnotation) { - assertThat(string).as("Spring uses Enum#name()").contains("method={GET, POST}"); + assertThat(string).as("Spring formatting") + .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") + .contains("method={GET, POST}", + "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", + "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", + "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", + "shortValue=9876", "shorts={9876}", + "longValue=42L", "longs={42L}", + "floatValue=3.14f", "floats={3.14f}", + "doubleValue=99.999d", "doubles={99.999d}" + ); } else { - assertThat(string).as("JDK uses Enum#toString()").contains("method={method: get, method: post}"); + assertThat(string).as("JDK 9-18 formatting") + .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests$RequestMapping(") + .contains("method={method: get, method: post}", + "clazz=org.springframework.core.annotation.MergedAnnotationsTests$RequestMethod.class", + "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests$RequestMethod[].class}", + "shortValue=9876", "shorts={9876}", + "floatValue=3.14f", "floats={3.14f}", + "doubleValue=99.999", "doubles={99.999}" + ); + if (JRE.currentVersion().ordinal() < JRE.JAVA_14.ordinal()) { + assertThat(string).as("JDK 9-13 formatting") + .contains("longValue=42", "longs={42}", + "byteValue=-1", "bytes={-1}" + ); + } + else { + assertThat(string).as("JDK 14+ formatting") + .contains("longValue=42L", "longs={42L}", + "byteValue=(byte)0xff", "bytes={(byte)0xff}" + ); + } } } @@ -2985,8 +3015,29 @@ class MergedAnnotationsTests { RequestMethod[] method() default {}; - // clazz is only used for testing annotation toString() implementations - Class clazz() default Object.class; + // --------------------------------------------------------------------- + // All remaining attributes declare default values that are used solely + // for the purpose of testing the toString() implementations for annotations. + Class clazz() default RequestMethod.class; + Class[] classes() default {int[][].class, RequestMethod[].class}; + + char ch() default 'X'; + char[] chars() default {'X'}; + + byte byteValue() default (byte) 0xFF; + byte[] bytes() default {(byte) 0xFF}; + + short shortValue() default 9876; + short[] shorts() default {9876}; + + long longValue() default 42L; + long[] longs() default {42L}; + + float floatValue() default 3.14F; + float[] floats() default {3.14F}; + + double doubleValue() default 99.999D; + double[] doubles() default {99.999D}; } @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java index 7ed75786f8f..5c409d88044 100644 --- a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,8 +72,8 @@ class BootstrapUtilsTests { assertThatIllegalStateException().isThrownBy(() -> resolveTestContextBootstrapper(bootstrapContext)) .withMessageContaining("Configuration error: found multiple declarations of @BootstrapWith") - .withMessageContaining(FooBootstrapper.class.getName()) - .withMessageContaining(BarBootstrapper.class.getName()); + .withMessageContaining(FooBootstrapper.class.getCanonicalName()) + .withMessageContaining(BarBootstrapper.class.getCanonicalName()); } @Test