Skip to content

Commit e7cc195

Browse files
committed
ReflectiveMethodResolver reliably prefers direct matches over vararg methods
Issue: SPR-12803
1 parent 474862a commit e7cc195

File tree

3 files changed

+95
-48
lines changed

3 files changed

+95
-48
lines changed

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

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -42,13 +42,14 @@
4242
public class ReflectionHelper {
4343

4444
/**
45-
* Compare argument arrays and return information about whether they match. A supplied
46-
* type converter and conversionAllowed flag allow for matches to take into account
47-
* that a type may be transformed into a different type by the converter.
48-
* @param expectedArgTypes the array of types the method/constructor is expecting
49-
* @param suppliedArgTypes the array of types that are being supplied at the point of invocation
45+
* Compare argument arrays and return information about whether they match.
46+
* A supplied type converter and conversionAllowed flag allow for matches to take
47+
* into account that a type may be transformed into a different type by the converter.
48+
* @param expectedArgTypes the types the method/constructor is expecting
49+
* @param suppliedArgTypes the types that are being supplied at the point of invocation
5050
* @param typeConverter a registered type converter
51-
* @return a MatchInfo object indicating what kind of match it was or null if it was not a match
51+
* @return a MatchInfo object indicating what kind of match it was,
52+
* or {@code null} if it was not a match
5253
*/
5354
static ArgumentsMatchInfo compareArguments(
5455
List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
@@ -111,7 +112,7 @@ public static int getTypeDifferenceWeight(List<TypeDescriptor> paramTypes, List<
111112
int result = 0;
112113
for (int i = 0; i < paramTypes.size(); i++) {
113114
TypeDescriptor paramType = paramTypes.get(i);
114-
TypeDescriptor argType = argTypes.get(i);
115+
TypeDescriptor argType = (i < argTypes.size() ? argTypes.get(i) : null);
115116
if (argType == null) {
116117
if (paramType.isPrimitive()) {
117118
return Integer.MAX_VALUE;
@@ -148,13 +149,15 @@ else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) {
148149
}
149150

150151
/**
151-
* Compare argument arrays and return information about whether they match. A supplied type converter and
152-
* conversionAllowed flag allow for matches to take into account that a type may be transformed into a different
153-
* type by the converter. This variant of compareArguments also allows for a varargs match.
154-
* @param expectedArgTypes the array of types the method/constructor is expecting
155-
* @param suppliedArgTypes the array of types that are being supplied at the point of invocation
152+
* Compare argument arrays and return information about whether they match.
153+
* A supplied type converter and conversionAllowed flag allow for matches to
154+
* take into account that a type may be transformed into a different type by the
155+
* converter. This variant of compareArguments also allows for a varargs match.
156+
* @param expectedArgTypes the types the method/constructor is expecting
157+
* @param suppliedArgTypes the types that are being supplied at the point of invocation
156158
* @param typeConverter a registered type converter
157-
* @return a MatchInfo object indicating what kind of match it was or null if it was not a match
159+
* @return a MatchInfo object indicating what kind of match it was,
160+
* or {@code null} if it was not a match
158161
*/
159162
static ArgumentsMatchInfo compareArgumentsVarargs(
160163
List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
@@ -264,14 +267,15 @@ else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsPar
264267
}
265268

266269
/**
267-
* Takes an input set of argument values and, following the positions specified in the int array,
268-
* it converts them to the types specified as the required parameter types. The arguments are
269-
* converted 'in-place' in the input array.
270+
* Takes an input set of argument values and converts them to the types specified as the
271+
* required parameter types. The arguments are converted 'in-place' in the input array.
270272
* @param converter the type converter to use for attempting conversions
271273
* @param arguments the actual arguments that need conversion
272274
* @param methodOrCtor the target Method or Constructor
273-
* @param argumentsRequiringConversion details which of the input arguments for sure need conversion
275+
* @param argumentsRequiringConversion details which of the input arguments need conversion
274276
* @param varargsPosition the known position of the varargs argument, if any
277+
* ({@code null} if not varargs)
278+
* @return {@code true} if some kind of conversion occurred on an argument
275279
* @throws EvaluationException if a problem occurs during conversion
276280
*/
277281
static void convertArguments(TypeConverter converter, Object[] arguments, Object methodOrCtor,
@@ -285,18 +289,21 @@ static void convertArguments(TypeConverter converter, Object[] arguments, Object
285289
}
286290
}
287291
else {
292+
// Convert everything up to the varargs position
288293
for (int i = 0; i < varargsPosition; i++) {
289294
TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
290295
Object argument = arguments[i];
291296
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
292297
}
293298
MethodParameter methodParam = MethodParameter.forMethodOrConstructor(methodOrCtor, varargsPosition);
294299
if (varargsPosition == arguments.length - 1) {
300+
// If the target is varargs and there is just one more argument then convert it here
295301
TypeDescriptor targetType = new TypeDescriptor(methodParam);
296302
Object argument = arguments[varargsPosition];
297303
arguments[varargsPosition] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
298304
}
299305
else {
306+
// Convert remaining arguments to the varargs element type
300307
TypeDescriptor targetType = TypeDescriptor.nested(methodParam, 1);
301308
for (int i = varargsPosition; i < arguments.length; i++) {
302309
Object argument = arguments[i];
@@ -307,12 +314,14 @@ static void convertArguments(TypeConverter converter, Object[] arguments, Object
307314
}
308315

309316
/**
310-
* Convert a supplied set of arguments into the requested types. If the parameterTypes are related to
311-
* a varargs method then the final entry in the parameterTypes array is going to be an array itself whose
312-
* component type should be used as the conversion target for extraneous arguments. (For example, if the
313-
* parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both
314-
* the boolean and float must be converted to strings). This method does not repackage the arguments
315-
* into a form suitable for the varargs invocation
317+
* Convert a supplied set of arguments into the requested types.
318+
* If the parameterTypes are related to a varargs method, then the final entry
319+
* in the parameterTypes array is going to be an array itself whose component type
320+
* should be used as the conversion target for extraneous arguments. (For example,
321+
* if the parameterTypes are {@code {Integer, String[]}} and the input arguments
322+
* are {@code {Integer, boolean, float}} then both the boolean and float must be
323+
* converted to Strings). This method does not repackage the arguments into a
324+
* form suitable for the varargs invocation.
316325
* @param converter the converter to use for type conversions
317326
* @param arguments the arguments to convert to the requested parameter types
318327
* @param method the target Method
@@ -359,10 +368,10 @@ public static void convertAllArguments(TypeConverter converter, Object[] argumen
359368
}
360369

361370
/**
362-
* Package up the arguments so that they correctly match what is expected in parameterTypes. For example, if
363-
* parameterTypes is (int, String[]) because the second parameter was declared String... then if arguments is
364-
* [1,"a","b"] then it must be repackaged as [1,new String[]{"a","b"}] in order to match the expected
365-
* parameterTypes.
371+
* Package up the arguments so that they correctly match what is expected in parameterTypes.
372+
* For example, if parameterTypes is {@code (int, String[])} because the second parameter
373+
* was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
374+
* repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
366375
* @param requiredParameterTypes the types of the parameters for the invocation
367376
* @param args the arguments to be setup ready for the invocation
368377
* @return a repackaged array of arguments where any varargs setup has been done
@@ -372,7 +381,7 @@ public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredPar
372381
int parameterCount = requiredParameterTypes.length;
373382
int argumentCount = args.length;
374383

375-
// Check if repackaging is needed:
384+
// Check if repackaging is needed...
376385
if (parameterCount != args.length ||
377386
requiredParameterTypes[parameterCount - 1] !=
378387
(args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {
@@ -421,10 +430,11 @@ public static enum ArgsMatchKind {
421430

422431

423432
/**
424-
* An instance of ArgumentsMatchInfo describes what kind of match was achieved between two sets of arguments -
425-
* the set that a method/constructor is expecting and the set that are being supplied at the point of invocation.
426-
* If the kind indicates that conversion is required for some of the arguments then the arguments that require
427-
* conversion are listed in the argsRequiringConversion array.
433+
* An instance of ArgumentsMatchInfo describes what kind of match was achieved
434+
* between two sets of arguments - the set that a method/constructor is expecting
435+
* and the set that are being supplied at the point of invocation. If the kind
436+
* indicates that conversion is required for some of the arguments then the arguments
437+
* that require conversion are listed in the argsRequiringConversion array.
428438
*/
429439
public static class ArgumentsMatchInfo {
430440

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

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -66,13 +66,14 @@ public ReflectiveMethodResolver() {
6666
}
6767

6868
/**
69-
* This constructors allows the ReflectiveMethodResolver to be configured such that it will
70-
* use a distance computation to check which is the better of two close matches (when there
71-
* are multiple matches). Using the distance computation is intended to ensure matches
72-
* are more closely representative of what a Java compiler would do when taking into
73-
* account boxing/unboxing and whether the method candidates are declared to handle a
74-
* supertype of the type (of the argument) being passed in.
75-
* @param useDistance true if distance computation should be used when calculating matches
69+
* This constructor allows the ReflectiveMethodResolver to be configured such that it
70+
* will use a distance computation to check which is the better of two close matches
71+
* (when there are multiple matches). Using the distance computation is intended to
72+
* ensure matches are more closely representative of what a Java compiler would do
73+
* when taking into account boxing/unboxing and whether the method candidates are
74+
* declared to handle a supertype of the type (of the argument) being passed in.
75+
* @param useDistance {@code true} if distance computation should be used when
76+
* calculating matches; {@code false} otherwise
7677
*/
7778
public ReflectiveMethodResolver(boolean useDistance) {
7879
this.useDistance = useDistance;
@@ -122,7 +123,19 @@ public MethodExecutor resolve(EvaluationContext context, Object targetObject, St
122123
public int compare(Method m1, Method m2) {
123124
int m1pl = m1.getParameterTypes().length;
124125
int m2pl = m2.getParameterTypes().length;
125-
return (new Integer(m1pl)).compareTo(m2pl);
126+
// varargs methods go last
127+
if (m1pl == m2pl) {
128+
if (!m1.isVarArgs() && m2.isVarArgs()) {
129+
return -1;
130+
}
131+
else if (m1.isVarArgs() && !m2.isVarArgs()) {
132+
return 1;
133+
}
134+
else {
135+
return 0;
136+
}
137+
}
138+
return (m1pl < m2pl ? -1 : (m1pl > m2pl ? 1 : 0));
126139
}
127140
});
128141
}
@@ -162,14 +175,17 @@ else if (paramTypes.length == argumentTypes.size()) {
162175
return new ReflectiveMethodExecutor(method, null);
163176
}
164177
else if (matchInfo.isCloseMatch()) {
165-
if (!this.useDistance) {
166-
closeMatch = method;
167-
}
168-
else {
178+
if (this.useDistance) {
169179
int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes);
170-
if (matchDistance < closeMatchDistance) {
180+
if (closeMatch == null || matchDistance < closeMatchDistance) {
171181
// This is a better match...
182+
closeMatch = method;
172183
closeMatchDistance = matchDistance;
184+
}
185+
}
186+
else {
187+
// Take this as a close match if there isn't one already
188+
if (closeMatch == null) {
173189
closeMatch = method;
174190
}
175191
}

spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -1843,6 +1843,15 @@ public void SPR12502() throws Exception {
18431843
assertEquals(NamedUser.class.getName(), expression.getValue(new NamedUser()));
18441844
}
18451845

1846+
@Test
1847+
public void SPR12803() throws Exception {
1848+
StandardEvaluationContext sec = new StandardEvaluationContext();
1849+
sec.setVariable("iterable", Collections.emptyList());
1850+
SpelExpressionParser parser = new SpelExpressionParser();
1851+
Expression expression = parser.parseExpression("T(org.springframework.expression.spel.SpelReproTests.GuavaLists).newArrayList(#iterable)");
1852+
assertTrue(expression.getValue(sec) instanceof ArrayList);
1853+
}
1854+
18461855

18471856
private static enum ABC { A, B, C }
18481857

@@ -1996,4 +2005,16 @@ public String getName() {
19962005
}
19972006
}
19982007

2008+
2009+
public static class GuavaLists {
2010+
2011+
public static <T> List<T> newArrayList(Iterable<T> iterable) {
2012+
return new ArrayList<T>();
2013+
}
2014+
2015+
public static <T> List<T> newArrayList(Object... elements) {
2016+
throw new UnsupportedOperationException();
2017+
}
2018+
}
2019+
19992020
}

0 commit comments

Comments
 (0)