From 6afb65b225df5211365647f68090fb10a02afbba Mon Sep 17 00:00:00 2001 From: Chengang Guan <115277968+guanchengang@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:37:25 +0800 Subject: [PATCH 1/2] Fix single-value adaptation for primitive array types in TypeMappedAnnotation Prior to this commit, `adaptForAttribute(Method, Object)` created the wrapping array from `value.getClass()` when a single non-array value was provided for an array attribute. This worked for object array types but failed for primitive array types: wrapping a boxed value produced a boxed array, which then failed the compatibility check and threw an `IllegalStateException`. This commit derives the component type from the declared attribute type when it is assignable from the value type, falling back to `value.getClass()` otherwise. The existing adaptation path for object array types is therefore preserved, and all primitive array types now accept a single value. The accompanying test covers single-value wrapping for every array type declared by ArrayTypes. Closes gh-37349 Signed-off-by: Chengang Guan --- .../core/annotation/TypeMappedAnnotation.java | 4 +- .../annotation/TypeMappedAnnotationTests.java | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java index 781078f6580..69b02fe6fe3 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -488,7 +488,9 @@ final class TypeMappedAnnotation extends AbstractMergedAnn private Object adaptForAttribute(Method attribute, Object value) { Class attributeType = ClassUtils.resolvePrimitiveIfNecessary(attribute.getReturnType()); if (attributeType.isArray() && !value.getClass().isArray()) { - Object array = Array.newInstance(value.getClass(), 1); + Object array = ClassUtils.isAssignable(attributeType.getComponentType(), value.getClass()) ? + Array.newInstance(attributeType.getComponentType(), 1) : + Array.newInstance(value.getClass(), 1); Array.set(array, 0, value); return adaptForAttribute(attribute, array); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java index 087dc23ed66..684a3859d01 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; @@ -42,6 +43,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.throwable; * * @author Phillip Webb * @author Sam Brannen + * @author Chengang Guan */ class TypeMappedAnnotationTests { @@ -109,6 +111,46 @@ class TypeMappedAnnotationTests { assertThat(annotation.getValue("enumValue")).contains(new ExampleEnum[] {}); } + @Test + @SuppressWarnings("unchecked") + void adaptFromSingleValueToArray() { + Map attributes = new HashMap<>(); + attributes.put("stringValue", "s"); + attributes.put("byteValue", (byte) 1); + attributes.put("shortValue", (short) 1); + attributes.put("intValue", 1); + attributes.put("longValue", 1L); + attributes.put("booleanValue", true); + attributes.put("charValue", 'c'); + attributes.put("doubleValue", 1.0d); + attributes.put("floatValue", 1.0f); + attributes.put("classValue", String.class); + attributes.put("enumValue", ExampleEnum.ONE); + attributes.put("annotationValue", TypeMappedAnnotation.of(null, null, + ExplicitMirror.class, Collections.singletonMap("a", "singleValue"))); + + MergedAnnotation annotation = TypeMappedAnnotation.of(null, null, ArrayTypes.class, attributes); + assertThat(annotation.getValue("stringValue")).contains(new String[] {"s"}); + assertThat(annotation.getValue("byteValue")).contains(new byte[] {1}); + assertThat(annotation.getValue("shortValue")).contains(new short[] {1}); + assertThat(annotation.getValue("intValue")).contains(new int[] {1}); + assertThat(annotation.getValue("longValue")).contains(new long[] {1L}); + assertThat(annotation.getValue("booleanValue")).contains(new boolean[] {true}); + assertThat(annotation.getValue("charValue")).contains(new char[] {'c'}); + assertThat(annotation.getValue("doubleValue")).contains(new double[] {1.0d}); + assertThat(annotation.getValue("floatValue")).contains(new float[] {1.0f}); + assertThat(annotation.getValue("classValue")).contains(new Class[] {String.class}); + assertThat(annotation.getValue("enumValue")).contains(new ExampleEnum[] {ExampleEnum.ONE}); + assertThat(annotation.getValue("annotationValue")).hasValueSatisfying(value -> { + assertThat(value.getClass().isArray()).isTrue(); + assertThat(Array.getLength(value)).isEqualTo(1); + Object element = Array.get(value, 0); + MergedAnnotation mirror = (MergedAnnotation) element; + assertThat(mirror.getString("a")).isEqualTo("singleValue"); + assertThat(mirror.getString("b")).isEqualTo("singleValue"); + }); + } + @Test void adaptFromNestedMergedAnnotation() { MergedAnnotation nested = MergedAnnotation.of(NestedAnnotation.class); From 043442a2a1c29dc4e7252377911f02b4ca935580 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:38:56 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Test=20single-value=20to=20primitive=20arra?= =?UTF-8?q?y=20adaptation=20via=20@=E2=81=A0AliasFor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a test to MergedAnnotationsTests that verifies a single primitive attribute in a composed annotation can be aliased via @⁠AliasFor to a primitive array attribute in a meta-annotation. See gh-37349 --- .../annotation/MergedAnnotationsTests.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) 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 61fc0ffe367..0a2d5595da8 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 @@ -596,6 +596,19 @@ class MergedAnnotationsTests { assertThat(annotation.getClassArray("classes")).isEmpty(); } + @Test + void getWithSingleValuePrimitiveAttributesOverridingPrimitiveArraysViaAliasFor() { + MergedAnnotation annotation = MergedAnnotations.from(ComposedPrimitiveSingleValuesClass.class) + .get(PrimitiveArrays.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getIntArray("ints")).containsExactly(42); + assertThat(annotation.getLongArray("longs")).containsExactly(42L); + assertThat(annotation.getBooleanArray("booleans")).containsExactly(true); + assertThat(annotation.getCharArray("chars")).containsExactly('c'); + assertThat(annotation.getDoubleArray("doubles")).containsExactly(4.2d); + assertThat(annotation.synthesize().ints()).containsExactly(42); + } + @Test void getWithInheritedAnnotationsFromShadowedAliasComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( @@ -2274,6 +2287,44 @@ class MergedAnnotationsTests { @interface ComposedTransactionalComponent { } + @Retention(RetentionPolicy.RUNTIME) + @interface PrimitiveArrays { + + int[] ints() default {}; + + long[] longs() default {}; + + boolean[] booleans() default {}; + + char[] chars() default {}; + + double[] doubles() default {}; + } + + @PrimitiveArrays + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedPrimitiveSingleValues { + + @AliasFor(annotation = PrimitiveArrays.class, attribute = "ints") + int anInt() default 0; + + @AliasFor(annotation = PrimitiveArrays.class, attribute = "longs") + long aLong() default 0L; + + @AliasFor(annotation = PrimitiveArrays.class, attribute = "booleans") + boolean aBoolean() default false; + + @AliasFor(annotation = PrimitiveArrays.class, attribute = "chars") + char aChar() default 'x'; + + @AliasFor(annotation = PrimitiveArrays.class, attribute = "doubles") + double aDouble() default 0.0d; + } + + @ComposedPrimitiveSingleValues(anInt = 42, aLong = 42L, aBoolean = true, aChar = 'c', aDouble = 4.2d) + static class ComposedPrimitiveSingleValuesClass { + } + static class NonAnnotatedClass { }