Browse Source

Revise compilation support in SpEL for varargs array subtypes

This commit first reverts changes to SpelNodeImpl from the previous
commit in order to reduce the scope of the overall change set.

This commit then implements a different approach to support type-safe
checks for array subtype compatibility.

In order to support backward compatibility, this commit also
reintroduces generateCodeForArguments(MethodVisitor, CodeFlow, Member,
SpelNodeImpl[]) in deprecated form.

See gh-32804
pull/32829/head
Sam Brannen 2 years ago
parent
commit
8fe4493a7d
  1. 107
      spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java
  2. 15
      spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java

107
spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java

@ -17,11 +17,12 @@
package org.springframework.expression.spel.ast; package org.springframework.expression.spel.ast;
import java.lang.reflect.Executable; import java.lang.reflect.Executable;
import java.util.Objects; import java.lang.reflect.Member;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor; import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes; import org.springframework.asm.Opcodes;
import org.springframework.asm.Type;
import org.springframework.expression.EvaluationException; import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue; import org.springframework.expression.TypedValue;
import org.springframework.expression.common.ExpressionUtils; import org.springframework.expression.common.ExpressionUtils;
@ -32,7 +33,9 @@ import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.SpelNode;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/** /**
* The common supertype of all AST nodes in a parsed Spring Expression Language * The common supertype of all AST nodes in a parsed Spring Expression Language
@ -213,65 +216,95 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
* and if it is then the argument values will be appropriately packaged into an array. * and if it is then the argument values will be appropriately packaged into an array.
* @param mv the method visitor where code should be generated * @param mv the method visitor where code should be generated
* @param cf the current codeflow * @param cf the current codeflow
* @param executable the method or constructor for which arguments are being set up * @param member the method or constructor for which arguments are being set up
* @param arguments the expression nodes for the expression supplied argument values * @param arguments the expression nodes for the expression supplied argument values
* @deprecated As of Spring Framework 6.2, in favor of
* {@link #generateCodeForArguments(MethodVisitor, CodeFlow, Executable, SpelNodeImpl[])}
*/
@Deprecated(since = "6.2")
protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Member member, SpelNodeImpl[] arguments) {
if (member instanceof Executable executable) {
generateCodeForArguments(mv, cf, executable, arguments);
}
throw new IllegalArgumentException(
"The supplied member must be an instance of java.lang.reflect.Executable: " + member);
}
/**
* Generate code that handles building the argument values for the specified
* {@link Executable} (method or constructor).
* <p>This method takes into account whether the invoked executable was
* declared to accept varargs, and if it was then the argument values will be
* appropriately packaged into an array.
* @param mv the method visitor where code should be generated
* @param cf the current {@link CodeFlow}
* @param executable the {@link Executable} (method or constructor) for which
* arguments are being set up
* @param arguments the expression nodes for the expression supplied argument
* values
* @since 6.2
*/ */
protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Executable executable, SpelNodeImpl[] arguments) { protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Executable executable, SpelNodeImpl[] arguments) {
String[] paramDescriptors = CodeFlow.toDescriptors(executable.getParameterTypes()); Class<?>[] parameterTypes = executable.getParameterTypes();
int paramCount = paramDescriptors.length; String[] paramDescriptors = CodeFlow.toDescriptors(parameterTypes);
if (executable.isVarArgs()) { if (executable.isVarArgs()) {
// The final parameter may or may not need packaging into an array, or nothing may // The final parameter may or may not need packaging into an array, or nothing may
// have been passed to satisfy the varargs and so something needs to be built. // have been passed to satisfy the varargs and so something needs to be built.
int p = 0; // Current supplied argument being processed
int childCount = arguments.length;
// Fulfill all the parameter requirements except the last one // Fulfill all the parameter requirements except the last one
for (int i = 0; i < paramCount - 1; i++) { for (p = 0; p < paramDescriptors.length - 1; p++) {
cf.generateCodeForArgument(mv, arguments[i], paramDescriptors[i]); cf.generateCodeForArgument(mv, arguments[p], paramDescriptors[p]);
} }
int argCount = arguments.length; SpelNodeImpl lastChild = (childCount == 0 ? null : arguments[childCount - 1]);
String varargsType = paramDescriptors[paramCount - 1]; ClassLoader classLoader = executable.getDeclaringClass().getClassLoader();
String varargsComponentType = varargsType.substring(1); // trim the leading '[', may leave other '[' Class<?> lastChildType = (lastChild != null ?
loadClassForExitDescriptor(lastChild.getExitDescriptor(), classLoader) : null);
Class<?> lastParameterType = parameterTypes[parameterTypes.length - 1];
if (needsVarargsArrayWrapping(arguments, paramDescriptors)) { // Determine if the final passed argument is already suitably packaged in array
// Package up the remaining arguments into an array // form to be passed to the method
CodeFlow.insertNewArrayCode(mv, argCount - paramCount + 1, varargsComponentType); if (lastChild != null && lastChildType != null && lastParameterType.isAssignableFrom(lastChildType)) {
for (int argIndex = paramCount - 1, arrayIndex = 0; argIndex < argCount; argIndex++, arrayIndex++) { cf.generateCodeForArgument(mv, lastChild, paramDescriptors[p]);
SpelNodeImpl child = arguments[argIndex];
mv.visitInsn(DUP);
CodeFlow.insertOptimalLoad(mv, arrayIndex);
cf.generateCodeForArgument(mv, child, varargsComponentType);
CodeFlow.insertArrayStore(mv, varargsComponentType);
}
}
else if (varargsType.equals(arguments[argCount - 1].getExitDescriptor())) {
// varargs type matches type of last argument
cf.generateCodeForArgument(mv, arguments[argCount - 1], paramDescriptors[paramCount - 1]);
} }
else { else {
// last argument is an array and varargs component type is a supertype of argument component type String arrayType = paramDescriptors[paramDescriptors.length - 1];
cf.generateCodeForArgument(mv, arguments[argCount - 1], varargsComponentType); arrayType = arrayType.substring(1); // trim the leading '[', may leave other '['
// build array big enough to hold remaining arguments
CodeFlow.insertNewArrayCode(mv, childCount - p, arrayType);
// Package up the remaining arguments into the array
int arrayindex = 0;
while (p < childCount) {
SpelNodeImpl child = arguments[p];
mv.visitInsn(DUP);
CodeFlow.insertOptimalLoad(mv, arrayindex++);
cf.generateCodeForArgument(mv, child, arrayType);
CodeFlow.insertArrayStore(mv, arrayType);
p++;
}
} }
} }
else { else {
for (int i = 0; i < paramCount; i++) { for (int i = 0; i < paramDescriptors.length;i++) {
cf.generateCodeForArgument(mv, arguments[i], paramDescriptors[i]); cf.generateCodeForArgument(mv, arguments[i], paramDescriptors[i]);
} }
} }
} }
private static boolean needsVarargsArrayWrapping(SpelNodeImpl[] arguments, String[] paramDescriptors) { @Nullable
if (arguments.length == 0) { private static Class<?> loadClassForExitDescriptor(@Nullable String exitDescriptor, ClassLoader classLoader) {
return true; if (!StringUtils.hasText(exitDescriptor)) {
return null;
} }
String typeDescriptor = exitDescriptor;
String lastExitTypeDescriptor = arguments[arguments.length - 1].exitTypeDescriptor; if (typeDescriptor.startsWith("[") || typeDescriptor.startsWith("L")) {
String lastParamDescriptor = paramDescriptors[paramDescriptors.length - 1]; typeDescriptor += ";";
return countLeadingOpeningBrackets(Objects.requireNonNull(lastExitTypeDescriptor)) != countLeadingOpeningBrackets(lastParamDescriptor); }
} String className = Type.getType(typeDescriptor).getClassName();
return ClassUtils.resolveClassName(className, classLoader);
private static long countLeadingOpeningBrackets(String lastExitTypeDescriptor) {
return lastExitTypeDescriptor.chars().takeWhile(c -> c == '[').count();
} }
/** /**

15
spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java

@ -4920,19 +4920,22 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
// varargs // varargs
expression = parser.parseExpression("new " + testclass8 + "(#root)"); expression = parser.parseExpression("new " + testclass8 + "(#root)");
Object[] objectArray = { "a", "b", "c" }; Object[] objectArray = { "a", "b", "c" };
assertThat(expression.getValue(objectArray).getClass().getName()).isEqualTo(testclass8); o = expression.getValue(objectArray);
assertThat(o).isExactlyInstanceOf(TestClass8.class);
assertCanCompile(expression); assertCanCompile(expression);
o = expression.getValue(objectArray); o = expression.getValue(objectArray);
assertThat(o.getClass().getName()).isEqualTo(testclass8); assertThat(o).isExactlyInstanceOf(TestClass8.class);
tc8 = (TestClass8) o; tc8 = (TestClass8) o;
assertThat(tc8.args).containsExactly("a", "b", "c"); assertThat(tc8.args).containsExactly("a", "b", "c");
// varargs with argument component type that is a subtype of the varargs component type. // varargs with argument component type that is a subtype of the varargs component type.
expression = parser.parseExpression("new " + testclass8 + "(#root)"); expression = parser.parseExpression("new " + testclass8 + "(#root)");
assertThat(expression.getValue(objectArray).getClass().getName()).isEqualTo(testclass8); String[] stringArray = { "a", "b", "c" };
o = expression.getValue(stringArray);
assertThat(o).isExactlyInstanceOf(TestClass8.class);
assertCanCompile(expression); assertCanCompile(expression);
o = expression.getValue(new String[] { "a", "b", "c" }); o = expression.getValue(stringArray);
assertThat(o.getClass().getName()).isEqualTo(testclass8); assertThat(o).isExactlyInstanceOf(TestClass8.class);
tc8 = (TestClass8) o; tc8 = (TestClass8) o;
assertThat(tc8.args).containsExactly("a", "b", "c"); assertThat(tc8.args).containsExactly("a", "b", "c");
@ -6939,7 +6942,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
s = a + "::"; s = a + "::";
} }
else { else {
s = a+"::"; s = a + "::";
for (Object varg: vargs) { for (Object varg: vargs) {
s += varg; s += varg;
} }

Loading…
Cancel
Save