Skip to content

Commit 380184e

Browse files
committed
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
1 parent 5cba32d commit 380184e

File tree

3 files changed

+154
-78
lines changed

3 files changed

+154
-78
lines changed

framework-docs/modules/ROOT/pages/core/expressions/language-ref/variables.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ characters.
2020
* dollar sign: `$`
2121
====
2222

23+
[TIP]
24+
====
25+
When setting a variable or root context object in the `EvaluationContext`, it is advised
26+
that the type of the variable or root context object be `public`.
27+
28+
Otherwise, certain types of SpEL expressions involving a variable or root context object
29+
with a non-public type may fail to evaluate or compile.
30+
====
31+
2332
[WARNING]
2433
====
2534
Since variables share a common namespace with

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,23 @@ public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationExceptio
7070

7171
@Override
7272
public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException {
73+
TypedValue result;
7374
if (THIS.equals(this.name)) {
74-
return state.getActiveContextObject();
75+
result = state.getActiveContextObject();
76+
// If the active context object (#this) is not the root context object (#root),
77+
// that means that #this is being evaluated within a nested scope (for example,
78+
// collection selection or collection project), which is not a compilable
79+
// expression, so we return the result without setting the exit type descriptor.
80+
if (result != state.getRootContextObject()) {
81+
return result;
82+
}
7583
}
76-
if (ROOT.equals(this.name)) {
77-
TypedValue result = state.getRootContextObject();
78-
this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue());
79-
return result;
84+
else if (ROOT.equals(this.name)) {
85+
result = state.getRootContextObject();
86+
}
87+
else {
88+
result = state.lookupVariable(this.name);
8089
}
81-
82-
TypedValue result = state.lookupVariable(this.name);
8390
setExitTypeDescriptor(result.getValue());
8491

8592
// A null value in the returned TypedValue will mean either the value was
@@ -132,7 +139,7 @@ public boolean isCompilable() {
132139

133140
@Override
134141
public void generateCode(MethodVisitor mv, CodeFlow cf) {
135-
if (ROOT.equals(this.name)) {
142+
if (THIS.equals(this.name) || ROOT.equals(this.name)) {
136143
mv.visitVarInsn(ALOAD, 1);
137144
}
138145
else {

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

Lines changed: 130 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.Field;
2121
import java.lang.reflect.Method;
22+
import java.lang.reflect.Modifier;
2223
import java.util.ArrayList;
2324
import java.util.Arrays;
2425
import java.util.Collection;
@@ -52,7 +53,6 @@
5253

5354
import static java.util.stream.Collectors.joining;
5455
import static org.assertj.core.api.Assertions.assertThat;
55-
import static org.assertj.core.api.Assertions.assertThatException;
5656
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5757
import static org.assertj.core.api.Assertions.within;
5858
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
@@ -124,7 +124,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
124124
* Further TODOs for compilation:
125125
*
126126
* - OpMinus with a single literal operand could be treated as a negative literal. Will save a
127-
* pointless loading of 0 and then a subtract instruction in code geneneration.
127+
* pointless loading of 0 and then a subtract instruction in code generation.
128128
*
129129
* - allow other accessors/resolvers to participate in compilation and create their own code.
130130
*
@@ -143,6 +143,84 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
143143
private SpelNodeImpl ast;
144144

145145

146+
@Nested
147+
class VariableReferenceTests {
148+
149+
@ParameterizedTest // gh-32356
150+
@ValueSource(strings = { "#root", "#this" })
151+
void rootVariableWithPublicType(String spel) {
152+
String string = "hello";
153+
expression = parser.parseExpression(spel);
154+
Object result = expression.getValue(string, String.class);
155+
assertThat(result).isEqualTo(string);
156+
assertCanCompile(expression);
157+
result = expression.getValue(string, String.class);
158+
assertThat(result).isEqualTo(string);
159+
160+
Integer number = 42;
161+
expression = parser.parseExpression(spel);
162+
result = expression.getValue(number, Integer.class);
163+
assertThat(result).isEqualTo(number);
164+
assertCanCompile(expression);
165+
result = expression.getValue(number, Integer.class);
166+
assertThat(result).isEqualTo(number);
167+
}
168+
169+
@ParameterizedTest // gh-32356
170+
@ValueSource(strings = {
171+
"#root.empty ? 0 : #root.size",
172+
"#this.empty ? 0 : #this.size"
173+
})
174+
void rootVariableWithNonPublicType(String spel) {
175+
Map<String, Integer> map = Map.of("a", 13, "b", 42);
176+
177+
// Prerequisite: root type must not be public for this use case.
178+
assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse();
179+
180+
expression = parser.parseExpression(spel);
181+
Integer result = expression.getValue(map, Integer.class);
182+
assertThat(result).isEqualTo(2);
183+
assertCanCompile(expression);
184+
result = expression.getValue(map, Integer.class);
185+
assertThat(result).isEqualTo(2);
186+
}
187+
188+
@Test
189+
void userDefinedVariable() {
190+
EvaluationContext ctx = new StandardEvaluationContext();
191+
ctx.setVariable("target", "abc");
192+
expression = parser.parseExpression("#target");
193+
assertThat(expression.getValue(ctx)).isEqualTo("abc");
194+
assertCanCompile(expression);
195+
assertThat(expression.getValue(ctx)).isEqualTo("abc");
196+
ctx.setVariable("target", "123");
197+
assertThat(expression.getValue(ctx)).isEqualTo("123");
198+
199+
// Changing the variable type from String to Integer results in a
200+
// ClassCastException in the compiled code.
201+
ctx.setVariable("target", 42);
202+
assertThatExceptionOfType(SpelEvaluationException.class)
203+
.isThrownBy(() -> expression.getValue(ctx))
204+
.withCauseInstanceOf(ClassCastException.class);
205+
206+
ctx.setVariable("target", "abc");
207+
expression = parser.parseExpression("#target.charAt(0)");
208+
assertThat(expression.getValue(ctx)).isEqualTo('a');
209+
assertCanCompile(expression);
210+
assertThat(expression.getValue(ctx)).isEqualTo('a');
211+
ctx.setVariable("target", "1");
212+
assertThat(expression.getValue(ctx)).isEqualTo('1');
213+
214+
// Changing the variable type from String to Integer results in a
215+
// ClassCastException in the compiled code.
216+
ctx.setVariable("target", 42);
217+
assertThatExceptionOfType(SpelEvaluationException.class)
218+
.isThrownBy(() -> expression.getValue(ctx))
219+
.withCauseInstanceOf(ClassCastException.class);
220+
}
221+
222+
}
223+
146224
@Nested
147225
class IndexingTests {
148226

@@ -466,10 +544,13 @@ void indexIntoMapOfListOfString() {
466544
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
467545
}
468546

469-
@Test
547+
@Test // gh-32356
470548
void indexIntoMapOfPrimitiveIntArray() {
471549
Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 });
472550

551+
// Prerequisite: root type must not be public for this use case.
552+
assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse();
553+
473554
// map key access
474555
expression = parser.parseExpression("['foo']");
475556

@@ -479,22 +560,37 @@ void indexIntoMapOfPrimitiveIntArray() {
479560
assertThat(stringify(expression.getValue(map))).isEqualTo("1 2 3");
480561
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
481562

482-
// map key access & array index
563+
// map key access via implicit #root & array index
483564
expression = parser.parseExpression("['foo'][1]");
484565

485566
assertThat(expression.getValue(map)).isEqualTo(2);
486567
assertCanCompile(expression);
487568
assertThat(expression.getValue(map)).isEqualTo(2);
569+
570+
// map key access via explicit #root & array index
571+
expression = parser.parseExpression("#root['foo'][1]");
572+
573+
assertThat(expression.getValue(map)).isEqualTo(2);
574+
assertCanCompile(expression);
575+
assertThat(expression.getValue(map)).isEqualTo(2);
576+
577+
// map key access via explicit #this & array index
578+
expression = parser.parseExpression("#this['foo'][1]");
579+
580+
assertThat(expression.getValue(map)).isEqualTo(2);
581+
assertCanCompile(expression);
582+
assertThat(expression.getValue(map)).isEqualTo(2);
488583
}
489584

490-
@Test
585+
@Test // gh-32356
491586
void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() {
492587
StandardEvaluationContext context = new StandardEvaluationContext();
493588
context.addPropertyAccessor(new CompilableMapAccessor());
494589

495-
// Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 });
496-
Map<String, int[]> map = new HashMap<>();
497-
map.put("foo", new int[] { 1, 2, 3 });
590+
Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 });
591+
592+
// Prerequisite: root type must not be public for this use case.
593+
assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse();
498594

499595
// map key access
500596
expression = parser.parseExpression("['foo']");
@@ -516,12 +612,13 @@ void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() {
516612

517613
assertThat(expression.getValue(context, map)).isEqualTo(2);
518614
assertCanCompile(expression);
519-
// TODO If map is created via Map.of(), the following fails with an IllegalAccessError.
520-
//
521-
// IllegalAccessError: failed to access class java.util.ImmutableCollections$Map1 from class
522-
// spel.Ex2774 (java.util.ImmutableCollections$Map1 is in module java.base of loader 'bootstrap';
523-
// spel.Ex2774 is in unnamed module of loader
524-
// org.springframework.expression.spel.standard.SpelCompiler$ChildClassLoader @359b650b)
615+
assertThat(expression.getValue(context, map)).isEqualTo(2);
616+
617+
// custom CompilableMapAccessor via explicit #this & array index
618+
expression = parser.parseExpression("#this.foo[1]");
619+
620+
assertThat(expression.getValue(context, map)).isEqualTo(2);
621+
assertCanCompile(expression);
525622
assertThat(expression.getValue(context, map)).isEqualTo(2);
526623

527624
// map key access & array index
@@ -644,7 +741,9 @@ void typeReference() {
644741
assertThat(expression.getValue()).isEqualTo(boolean.class);
645742

646743
expression = parse("T(Missing)");
647-
assertGetValueFail(expression);
744+
assertThatExceptionOfType(SpelEvaluationException.class)
745+
.isThrownBy(expression::getValue)
746+
.withMessageEndingWith("Type cannot be found 'Missing'");
648747
assertCantCompile(expression);
649748
}
650749

@@ -915,16 +1014,20 @@ void intLiteral() {
9151014

9161015
// Code gen is different for -1 .. 6 because there are bytecode instructions specifically for those values
9171016

918-
// Not an int literal but an opminus with one operand:
919-
// expression = parser.parseExpression("-1");
920-
// assertCanCompile(expression);
921-
// assertEquals(-1, expression.getValue());
1017+
// Not an int literal but an opMinus with one operand:
1018+
expression = parser.parseExpression("-1");
1019+
expression.getValue(Integer.class);
1020+
assertCanCompile(expression);
1021+
assertThat(expression.getValue()).isEqualTo(-1);
1022+
9221023
expression = parser.parseExpression("0");
9231024
assertCanCompile(expression);
9241025
assertThat(expression.getValue()).isEqualTo(0);
1026+
9251027
expression = parser.parseExpression("2");
9261028
assertCanCompile(expression);
9271029
assertThat(expression.getValue()).isEqualTo(2);
1030+
9281031
expression = parser.parseExpression("7");
9291032
assertCanCompile(expression);
9301033
assertThat(expression.getValue()).isEqualTo(7);
@@ -1374,23 +1477,6 @@ void elvis() {
13741477
assertCanCompile(expression);
13751478
}
13761479

1377-
@Test
1378-
void variableReference_root() {
1379-
String s = "hello";
1380-
Expression expression = parser.parseExpression("#root");
1381-
String resultI = expression.getValue(s, String.class);
1382-
assertCanCompile(expression);
1383-
String resultC = expression.getValue(s, String.class);
1384-
assertThat(resultI).isEqualTo(s);
1385-
assertThat(resultC).isEqualTo(s);
1386-
1387-
expression = parser.parseExpression("#root");
1388-
int i = (Integer) expression.getValue(42);
1389-
assertThat(i).isEqualTo(42);
1390-
assertCanCompile(expression);
1391-
i = (Integer) expression.getValue(42);
1392-
assertThat(i).isEqualTo(42);
1393-
}
13941480

13951481
public static String concat(String a, String b) {
13961482
return a+b;
@@ -1776,34 +1862,6 @@ void functionReferenceVarargs() throws Exception {
17761862
assertThat(expression.getValue(ctx)).isEqualTo("abc");
17771863
}
17781864

1779-
@Test
1780-
void variableReference_userDefined() {
1781-
EvaluationContext ctx = new StandardEvaluationContext();
1782-
ctx.setVariable("target", "abc");
1783-
expression = parser.parseExpression("#target");
1784-
assertThat(expression.getValue(ctx)).isEqualTo("abc");
1785-
assertCanCompile(expression);
1786-
assertThat(expression.getValue(ctx)).isEqualTo("abc");
1787-
ctx.setVariable("target", "123");
1788-
assertThat(expression.getValue(ctx)).isEqualTo("123");
1789-
ctx.setVariable("target", 42);
1790-
assertThatExceptionOfType(SpelEvaluationException.class)
1791-
.isThrownBy(() -> expression.getValue(ctx))
1792-
.withCauseInstanceOf(ClassCastException.class);
1793-
1794-
ctx.setVariable("target", "abc");
1795-
expression = parser.parseExpression("#target.charAt(0)");
1796-
assertThat(expression.getValue(ctx)).isEqualTo('a');
1797-
assertCanCompile(expression);
1798-
assertThat(expression.getValue(ctx)).isEqualTo('a');
1799-
ctx.setVariable("target", "1");
1800-
assertThat(expression.getValue(ctx)).isEqualTo('1');
1801-
ctx.setVariable("target", 42);
1802-
assertThatExceptionOfType(SpelEvaluationException.class)
1803-
.isThrownBy(() -> expression.getValue(ctx))
1804-
.withCauseInstanceOf(ClassCastException.class);
1805-
}
1806-
18071865
@Test
18081866
void opLt() {
18091867
expression = parse("3.0d < 4.0d");
@@ -5402,21 +5460,23 @@ private SpelNodeImpl getAst() {
54025460
}
54035461

54045462
private void assertCanCompile(Expression expression) {
5405-
assertThat(SpelCompiler.compile(expression)).isTrue();
5463+
assertThat(SpelCompiler.compile(expression))
5464+
.as(() -> "Expression <%s> should be compilable"
5465+
.formatted(((SpelExpression) expression).toStringAST()))
5466+
.isTrue();
54065467
}
54075468

54085469
private void assertCantCompile(Expression expression) {
5409-
assertThat(SpelCompiler.compile(expression)).isFalse();
5470+
assertThat(SpelCompiler.compile(expression))
5471+
.as(() -> "Expression <%s> should not be compilable"
5472+
.formatted(((SpelExpression) expression).toStringAST()))
5473+
.isFalse();
54105474
}
54115475

54125476
private Expression parse(String expression) {
54135477
return parser.parseExpression(expression);
54145478
}
54155479

5416-
private void assertGetValueFail(Expression expression) {
5417-
assertThatException().isThrownBy(expression::getValue);
5418-
}
5419-
54205480

54215481
// Nested types
54225482

0 commit comments

Comments
 (0)