Browse Source

Use ConversionService to convert POJO to array for SpEL varargs invocations

Prior to this commit, if an appropriate Converter was registered with
the ConversionService that converts from a POJO to an array and that
ConversionService was registered with the Spring Expression Language
(SpEL) TypeConverter, an attempt to invoke a varargs method in a SpEL
expression with such a POJO would fail because the ConversionService
was not used to convert the POJO to an array suitable for the varargs
method invocation.

This commit revises the implementations of convertArguments(...) and
convertAllMethodHandleArguments(...) in ReflectionHelper to support
such use cases.

Closes gh-34371
pull/34656/head
Sam Brannen 10 months ago
parent
commit
b07217ab67
  1. 10
      spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java
  2. 7
      spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java
  3. 82
      spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java

10
spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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:

7
spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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));

82
spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java

@ -16,12 +16,21 @@ @@ -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 { @@ -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<ConvertiblePair> 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();
}
}
}
}

Loading…
Cancel
Save