From c1236a33401e87ee486d050495e318b8a4cc08ac Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:27:07 +0100 Subject: [PATCH] Support varargs-only MethodHandle as SpEL function Prior to this commit, if a MethodHandle was registered as a custom function in the Spring Expression Language (SpEL) for a static method that accepted only a variable argument list (for example, `static String func(String... args)`), attempting to invoke the registered function within a SpEL expression resulted in a ClassCastException because the varargs array was unnecessarily wrapped in an Object[]. This commit modifies the logic in FunctionReference's internal executeFunctionViaMethodHandle() method to address that. Closes gh-34109 --- .../spel/ast/FunctionReference.java | 5 ++-- .../expression/spel/TestScenarioCreator.java | 7 +++++- .../spel/VariableAndFunctionTests.java | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 71c848820b5..20ee8e20b59 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -229,8 +229,9 @@ public class FunctionReference extends SpelNodeImpl { ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition); if (isSuspectedVarargs) { - if (declaredParamCount == 1) { - // We only repackage the varargs if it is the ONLY argument -- for example, + if (declaredParamCount == 1 && !methodHandle.isVarargsCollector()) { + // We only repackage the arguments if the MethodHandle accepts a single + // argument AND the MethodHandle is not a "varargs collector" -- for example, // when we are dealing with a bound MethodHandle. functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation( methodHandle.type().parameterArray(), functionArgs); 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 895952f62a4..d9280f5be4b 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 @@ -108,11 +108,16 @@ class TestScenarioCreator { "formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class)); testContext.registerFunction("formatObjectVarargs", formatObjectVarargs); - // #formatObjectVarargs(format, args...) + // #formatPrimitiveVarargs(format, args...) MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class, "formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class)); testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs); + // #varargsFunctionHandle(args...) + MethodHandle varargsFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class, + "varargsFunction", MethodType.methodType(String.class, String[].class)); + testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle); + // #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 38d7d047f21..88f8b0b8b39 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 @@ -79,6 +79,8 @@ class VariableAndFunctionTests extends AbstractExpressionTests { @Test void functionWithVarargs() { + // static String varargsFunction(String... strings) -> Arrays.toString(strings) + evaluate("#varargsFunction()", "[]", String.class); evaluate("#varargsFunction(new String[0])", "[]", String.class); evaluate("#varargsFunction('a')", "[a]", String.class); @@ -241,6 +243,27 @@ class VariableAndFunctionTests extends AbstractExpressionTests { evaluate("#formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class); } + @Test // gh-34109 + void functionViaMethodHandleForStaticMethodThatAcceptsOnlyVarargs() { + // #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings) + + evaluate("#varargsFunctionHandle()", "[]", String.class); + evaluate("#varargsFunctionHandle(new String[0])", "[]", String.class); + evaluate("#varargsFunctionHandle('a')", "[a]", String.class); + evaluate("#varargsFunctionHandle('a','b','c')", "[a, b, c]", String.class); + evaluate("#varargsFunctionHandle(new String[]{'a','b','c'})", "[a, b, c]", String.class); + // Conversion from int to String + evaluate("#varargsFunctionHandle(25)", "[25]", String.class); + evaluate("#varargsFunctionHandle('b',25)", "[b, 25]", String.class); + evaluate("#varargsFunctionHandle(new int[]{1, 2, 3})", "[1, 2, 3]", String.class); + // Strings that contain a comma + evaluate("#varargsFunctionHandle('a,b')", "[a,b]", String.class); + evaluate("#varargsFunctionHandle('a', 'x,y', 'd')", "[a, x,y, d]", String.class); + // null values + evaluate("#varargsFunctionHandle(null)", "[null]", String.class); + evaluate("#varargsFunctionHandle('a',null,'b')", "[a, null, b]", String.class); + } + @Test void functionMethodMustBeStatic() throws Exception { SpelExpressionParser parser = new SpelExpressionParser();