diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index c45464e5451..bb230b6dd9f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -319,8 +319,8 @@ public abstract class ReflectionHelper { (sourceType.isArray() && !sourceType.isAssignableTo(targetType)) || (argument instanceof List)) { - TypeDescriptor targetTypeToUse = - (sourceType.isArray() || argument instanceof List ? targetType : componentTypeDesc); + TypeDescriptor targetTypeToUse = (sourceType.isArray() || argument instanceof List || + converter.canConvert(sourceType, targetType) ? targetType : componentTypeDesc); arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse); } // Possible outcomes of the above if-else block: @@ -420,8 +420,8 @@ public abstract class ReflectionHelper { (sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType)) || (argument instanceof List)) { - TypeDescriptor targetTypeToUse = - (sourceType.isArray() || argument instanceof List ? varargsArrayType : varargsComponentType); + TypeDescriptor targetTypeToUse = (sourceType.isArray() || argument instanceof List || + converter.canConvert(sourceType, varargsArrayType) ? varargsArrayType : varargsComponentType); arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse); } // Possible outcomes of the above if-else block: diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java index d9280f5be4b..a99d3f5aea3 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -118,6 +118,11 @@ class TestScenarioCreator { "varargsFunction", MethodType.methodType(String.class, String[].class)); testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle); + // #varargsObjectFunctionHandle(args...) + MethodHandle varargsObjectFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class, + "varargsObjectFunction", MethodType.methodType(String.class, Object[].class)); + testContext.registerFunction("varargsObjectFunctionHandle", varargsObjectFunctionHandle); + // #add(int, int) MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class, "add", MethodType.methodType(int.class, int.class, int.class)); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java index 3cdccda0073..4e5e22df0c5 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java @@ -16,12 +16,21 @@ package org.springframework.expression.spel; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -279,4 +288,75 @@ class VariableAndFunctionTests extends AbstractExpressionTests { public void nonStatic() { } + + @Nested // gh-34371 + class VarargsAndPojoToArrayConversionTests { + + private final StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + + private final ArrayHolder arrayHolder = new ArrayHolder("a", "b", "c"); + + + @BeforeEach + void setUp() { + DefaultConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(new ArrayHolderConverter()); + context.setTypeConverter(new StandardTypeConverter(conversionService)); + context.setVariable("arrayHolder", arrayHolder); + } + + @Test + void functionWithVarargsAndPojoToArrayConversion() { + // #varargsFunction: static String varargsFunction(String... strings) -> Arrays.toString(strings) + evaluate("#varargsFunction(#arrayHolder)", "[a, b, c]"); + + // #varargsObjectFunction: static String varargsObjectFunction(Object... args) -> Arrays.toString(args) + // + // Since ArrayHolder is an "instanceof Object" and Object is the varargs component type, + // we expect the ArrayHolder not to be converted to an array but rather to be passed + // "as is" as a single argument to the varargs method. + evaluate("#varargsObjectFunction(#arrayHolder)", "[" + arrayHolder.toString() + "]"); + } + + @Test + void functionWithVarargsAndPojoToArrayConversionViaMethodHandle() { + // #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings) + evaluate("#varargsFunctionHandle(#arrayHolder)", "[a, b, c]"); + + // #varargsObjectFunctionHandle: static String varargsObjectFunction(Object... args) -> Arrays.toString(args) + // + // Since ArrayHolder is an "instanceof Object" and Object is the varargs component type, + // we expect the ArrayHolder not to be converted to an array but rather to be passed + // "as is" as a single argument to the varargs method. + evaluate("#varargsObjectFunctionHandle(#arrayHolder)", "[" + arrayHolder.toString() + "]"); + } + + private void evaluate(String expression, Object expectedValue) { + Expression expr = parser.parseExpression(expression); + assertThat(expr).as("expression").isNotNull(); + Object value = expr.getValue(context); + assertThat(value).as("expression '" + expression + "'").isEqualTo(expectedValue); + } + + + record ArrayHolder(String... array) { + } + + static class ArrayHolderConverter implements GenericConverter { + + @Nullable + @Override + public Set getConvertibleTypes() { + return Set.of(new ConvertiblePair(ArrayHolder.class, Object[].class)); + } + + @Nullable + @Override + public String[] convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return ((ArrayHolder) source).array(); + } + } + + } + }