Support SpEL compilation of #root or #this with non-public type

Prior to this commit, if a Spring Expression Language (SpEL) expression
referenced the root context object via the #root or #this variable, we
inserted a checkcast in the generated byte code that cast the object to
its concrete type. However if the root context object's type was
non-public, that resulted in an IllegalAccessError when the compiled
byte code was executed.

VariableReference.getValueInternal() already contains a solution for
global variables which inserts a checkcast to Object in the generated
byte code instead of to the object's concrete non-public type.

This commit therefore applies the same logic to #root (or #this when
used to reference the root context object) that is already applied to
global variables.

Closes gh-32356
This commit is contained in:
Sam Brannen 2024-03-03 14:35:04 +01:00
parent 5cba32df32
commit 380184e85a
3 changed files with 154 additions and 78 deletions

View File

@ -20,6 +20,15 @@ characters.
* dollar sign: `$` * dollar sign: `$`
==== ====
[TIP]
====
When setting a variable or root context object in the `EvaluationContext`, it is advised
that the type of the variable or root context object be `public`.
Otherwise, certain types of SpEL expressions involving a variable or root context object
with a non-public type may fail to evaluate or compile.
====
[WARNING] [WARNING]
==== ====
Since variables share a common namespace with Since variables share a common namespace with

View File

@ -70,16 +70,23 @@ public class VariableReference extends SpelNodeImpl {
@Override @Override
public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException { public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException {
TypedValue result;
if (THIS.equals(this.name)) { if (THIS.equals(this.name)) {
return state.getActiveContextObject(); result = state.getActiveContextObject();
// If the active context object (#this) is not the root context object (#root),
// that means that #this is being evaluated within a nested scope (for example,
// collection selection or collection project), which is not a compilable
// expression, so we return the result without setting the exit type descriptor.
if (result != state.getRootContextObject()) {
return result;
}
} }
if (ROOT.equals(this.name)) { else if (ROOT.equals(this.name)) {
TypedValue result = state.getRootContextObject(); result = state.getRootContextObject();
this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue()); }
return result; else {
result = state.lookupVariable(this.name);
} }
TypedValue result = state.lookupVariable(this.name);
setExitTypeDescriptor(result.getValue()); setExitTypeDescriptor(result.getValue());
// A null value in the returned TypedValue will mean either the value was // A null value in the returned TypedValue will mean either the value was
@ -132,7 +139,7 @@ public class VariableReference extends SpelNodeImpl {
@Override @Override
public void generateCode(MethodVisitor mv, CodeFlow cf) { public void generateCode(MethodVisitor mv, CodeFlow cf) {
if (ROOT.equals(this.name)) { if (THIS.equals(this.name) || ROOT.equals(this.name)) {
mv.visitVarInsn(ALOAD, 1); mv.visitVarInsn(ALOAD, 1);
} }
else { else {

View File

@ -19,6 +19,7 @@ package org.springframework.expression.spel;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -52,7 +53,6 @@ import org.springframework.expression.spel.testdata.PersonInOtherPackage;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.within; import static org.assertj.core.api.Assertions.within;
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN; import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
@ -124,7 +124,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
* Further TODOs for compilation: * Further TODOs for compilation:
* *
* - OpMinus with a single literal operand could be treated as a negative literal. Will save a * - OpMinus with a single literal operand could be treated as a negative literal. Will save a
* pointless loading of 0 and then a subtract instruction in code geneneration. * pointless loading of 0 and then a subtract instruction in code generation.
* *
* - allow other accessors/resolvers to participate in compilation and create their own code. * - allow other accessors/resolvers to participate in compilation and create their own code.
* *
@ -143,6 +143,84 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
private SpelNodeImpl ast; private SpelNodeImpl ast;
@Nested
class VariableReferenceTests {
@ParameterizedTest // gh-32356
@ValueSource(strings = { "#root", "#this" })
void rootVariableWithPublicType(String spel) {
String string = "hello";
expression = parser.parseExpression(spel);
Object result = expression.getValue(string, String.class);
assertThat(result).isEqualTo(string);
assertCanCompile(expression);
result = expression.getValue(string, String.class);
assertThat(result).isEqualTo(string);
Integer number = 42;
expression = parser.parseExpression(spel);
result = expression.getValue(number, Integer.class);
assertThat(result).isEqualTo(number);
assertCanCompile(expression);
result = expression.getValue(number, Integer.class);
assertThat(result).isEqualTo(number);
}
@ParameterizedTest // gh-32356
@ValueSource(strings = {
"#root.empty ? 0 : #root.size",
"#this.empty ? 0 : #this.size"
})
void rootVariableWithNonPublicType(String spel) {
Map<String, Integer> map = Map.of("a", 13, "b", 42);
// Prerequisite: root type must not be public for this use case.
assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse();
expression = parser.parseExpression(spel);
Integer result = expression.getValue(map, Integer.class);
assertThat(result).isEqualTo(2);
assertCanCompile(expression);
result = expression.getValue(map, Integer.class);
assertThat(result).isEqualTo(2);
}
@Test
void userDefinedVariable() {
EvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target");
assertThat(expression.getValue(ctx)).isEqualTo("abc");
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo("abc");
ctx.setVariable("target", "123");
assertThat(expression.getValue(ctx)).isEqualTo("123");
// Changing the variable type from String to Integer results in a
// ClassCastException in the compiled code.
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target.charAt(0)");
assertThat(expression.getValue(ctx)).isEqualTo('a');
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo('a');
ctx.setVariable("target", "1");
assertThat(expression.getValue(ctx)).isEqualTo('1');
// Changing the variable type from String to Integer results in a
// ClassCastException in the compiled code.
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
}
}
@Nested @Nested
class IndexingTests { class IndexingTests {
@ -466,10 +544,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
} }
@Test @Test // gh-32356
void indexIntoMapOfPrimitiveIntArray() { void indexIntoMapOfPrimitiveIntArray() {
Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 }); Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 });
// Prerequisite: root type must not be public for this use case.
assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse();
// map key access // map key access
expression = parser.parseExpression("['foo']"); expression = parser.parseExpression("['foo']");
@ -479,22 +560,37 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(stringify(expression.getValue(map))).isEqualTo("1 2 3"); assertThat(stringify(expression.getValue(map))).isEqualTo("1 2 3");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// map key access & array index // map key access via implicit #root & array index
expression = parser.parseExpression("['foo'][1]"); expression = parser.parseExpression("['foo'][1]");
assertThat(expression.getValue(map)).isEqualTo(2); assertThat(expression.getValue(map)).isEqualTo(2);
assertCanCompile(expression); assertCanCompile(expression);
assertThat(expression.getValue(map)).isEqualTo(2); assertThat(expression.getValue(map)).isEqualTo(2);
// map key access via explicit #root & array index
expression = parser.parseExpression("#root['foo'][1]");
assertThat(expression.getValue(map)).isEqualTo(2);
assertCanCompile(expression);
assertThat(expression.getValue(map)).isEqualTo(2);
// map key access via explicit #this & array index
expression = parser.parseExpression("#this['foo'][1]");
assertThat(expression.getValue(map)).isEqualTo(2);
assertCanCompile(expression);
assertThat(expression.getValue(map)).isEqualTo(2);
} }
@Test @Test // gh-32356
void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() { void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() {
StandardEvaluationContext context = new StandardEvaluationContext(); StandardEvaluationContext context = new StandardEvaluationContext();
context.addPropertyAccessor(new CompilableMapAccessor()); context.addPropertyAccessor(new CompilableMapAccessor());
// Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 }); Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 });
Map<String, int[]> map = new HashMap<>();
map.put("foo", new int[] { 1, 2, 3 }); // Prerequisite: root type must not be public for this use case.
assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse();
// map key access // map key access
expression = parser.parseExpression("['foo']"); expression = parser.parseExpression("['foo']");
@ -516,12 +612,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(expression.getValue(context, map)).isEqualTo(2); assertThat(expression.getValue(context, map)).isEqualTo(2);
assertCanCompile(expression); assertCanCompile(expression);
// TODO If map is created via Map.of(), the following fails with an IllegalAccessError. assertThat(expression.getValue(context, map)).isEqualTo(2);
//
// IllegalAccessError: failed to access class java.util.ImmutableCollections$Map1 from class // custom CompilableMapAccessor via explicit #this & array index
// spel.Ex2774 (java.util.ImmutableCollections$Map1 is in module java.base of loader 'bootstrap'; expression = parser.parseExpression("#this.foo[1]");
// spel.Ex2774 is in unnamed module of loader
// org.springframework.expression.spel.standard.SpelCompiler$ChildClassLoader @359b650b) assertThat(expression.getValue(context, map)).isEqualTo(2);
assertCanCompile(expression);
assertThat(expression.getValue(context, map)).isEqualTo(2); assertThat(expression.getValue(context, map)).isEqualTo(2);
// map key access & array index // map key access & array index
@ -644,7 +741,9 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(expression.getValue()).isEqualTo(boolean.class); assertThat(expression.getValue()).isEqualTo(boolean.class);
expression = parse("T(Missing)"); expression = parse("T(Missing)");
assertGetValueFail(expression); assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(expression::getValue)
.withMessageEndingWith("Type cannot be found 'Missing'");
assertCantCompile(expression); assertCantCompile(expression);
} }
@ -915,16 +1014,20 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
// Code gen is different for -1 .. 6 because there are bytecode instructions specifically for those values // Code gen is different for -1 .. 6 because there are bytecode instructions specifically for those values
// Not an int literal but an opminus with one operand: // Not an int literal but an opMinus with one operand:
// expression = parser.parseExpression("-1"); expression = parser.parseExpression("-1");
// assertCanCompile(expression); expression.getValue(Integer.class);
// assertEquals(-1, expression.getValue()); assertCanCompile(expression);
assertThat(expression.getValue()).isEqualTo(-1);
expression = parser.parseExpression("0"); expression = parser.parseExpression("0");
assertCanCompile(expression); assertCanCompile(expression);
assertThat(expression.getValue()).isEqualTo(0); assertThat(expression.getValue()).isEqualTo(0);
expression = parser.parseExpression("2"); expression = parser.parseExpression("2");
assertCanCompile(expression); assertCanCompile(expression);
assertThat(expression.getValue()).isEqualTo(2); assertThat(expression.getValue()).isEqualTo(2);
expression = parser.parseExpression("7"); expression = parser.parseExpression("7");
assertCanCompile(expression); assertCanCompile(expression);
assertThat(expression.getValue()).isEqualTo(7); assertThat(expression.getValue()).isEqualTo(7);
@ -1374,23 +1477,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertCanCompile(expression); assertCanCompile(expression);
} }
@Test
void variableReference_root() {
String s = "hello";
Expression expression = parser.parseExpression("#root");
String resultI = expression.getValue(s, String.class);
assertCanCompile(expression);
String resultC = expression.getValue(s, String.class);
assertThat(resultI).isEqualTo(s);
assertThat(resultC).isEqualTo(s);
expression = parser.parseExpression("#root");
int i = (Integer) expression.getValue(42);
assertThat(i).isEqualTo(42);
assertCanCompile(expression);
i = (Integer) expression.getValue(42);
assertThat(i).isEqualTo(42);
}
public static String concat(String a, String b) { public static String concat(String a, String b) {
return a+b; return a+b;
@ -1776,34 +1862,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(expression.getValue(ctx)).isEqualTo("abc"); assertThat(expression.getValue(ctx)).isEqualTo("abc");
} }
@Test
void variableReference_userDefined() {
EvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target");
assertThat(expression.getValue(ctx)).isEqualTo("abc");
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo("abc");
ctx.setVariable("target", "123");
assertThat(expression.getValue(ctx)).isEqualTo("123");
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target.charAt(0)");
assertThat(expression.getValue(ctx)).isEqualTo('a');
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo('a');
ctx.setVariable("target", "1");
assertThat(expression.getValue(ctx)).isEqualTo('1');
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
}
@Test @Test
void opLt() { void opLt() {
expression = parse("3.0d < 4.0d"); expression = parse("3.0d < 4.0d");
@ -5402,21 +5460,23 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
} }
private void assertCanCompile(Expression expression) { private void assertCanCompile(Expression expression) {
assertThat(SpelCompiler.compile(expression)).isTrue(); assertThat(SpelCompiler.compile(expression))
.as(() -> "Expression <%s> should be compilable"
.formatted(((SpelExpression) expression).toStringAST()))
.isTrue();
} }
private void assertCantCompile(Expression expression) { private void assertCantCompile(Expression expression) {
assertThat(SpelCompiler.compile(expression)).isFalse(); assertThat(SpelCompiler.compile(expression))
.as(() -> "Expression <%s> should not be compilable"
.formatted(((SpelExpression) expression).toStringAST()))
.isFalse();
} }
private Expression parse(String expression) { private Expression parse(String expression) {
return parser.parseExpression(expression); return parser.parseExpression(expression);
} }
private void assertGetValueFail(Expression expression) {
assertThatException().isThrownBy(expression::getValue);
}
// Nested types // Nested types