mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-10-07 15:39:29 +00:00
Disable SpEL expression compilation by default in SimpleEvaluationContext
Prior to this commit, SpEL expression compilation could be silently activated in a SimpleEvaluationContext via the `spring.expression.compiler.mode` Spring/system property or SpelParserConfiguration. Once an expression is compiled, the evaluation guards enforced during interpreted evaluation are no longer applied, which is at odds with the restricted intent of SimpleEvaluationContext. To address that, this commit introduces a mechanism analogous to isAssignmentEnabled() which disables compilation by default in SimpleEvaluationContext. Specifically: - A new isCompilationSupported() default method has been introduced in the EvaluationContext API, which returns true by default. - SimpleEvaluationContext overrides isCompilationSupported() to return false by default. However, compilation can be opted into explicitly via the new withCompilationSupported() method in the SimpleEvaluationContext.Builder. - SpelExpression.checkCompile() now consults isCompilationSupported() before triggering new compilation, ensuring that evaluation within an EvaluationContext never produces a compiled form of the expression if the context's isCompilationSupported() method returns false. - All eight getValue() variants in SpelExpression now consult isCompilationSupported() before executing a compiled expression, ensuring that a compiled expression produced via a different EvaluationContext is not silently reused if the caller inadvertently switches to an EvaluationContext that does not support compilation. Closes gh-37035
This commit is contained in:
committed by
Brian Clozel
parent
baae93f20a
commit
0d08f8dfaf
+161
@@ -21,14 +21,18 @@ import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.SpelCompilerMode;
|
||||
import org.springframework.expression.spel.SpelParserConfiguration;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
|
||||
import static org.springframework.expression.spel.standard.SpelExpressionTestUtils.assertIsCompiled;
|
||||
import static org.springframework.expression.spel.standard.SpelExpressionTestUtils.assertIsNotCompiled;
|
||||
import static org.springframework.expression.spel.standard.SpelExpressionTestUtils.getInterpretedCount;
|
||||
|
||||
/**
|
||||
* Tests for the {@link SpelCompiler}.
|
||||
@@ -50,6 +54,7 @@ class SpelCompilerTests {
|
||||
|
||||
// Evaluate the expression multiple times to ensure that it gets compiled.
|
||||
IntStream.rangeClosed(1, 5).forEach(i -> assertThat(expression.getValue(component)).isEqualTo(42));
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test // gh-25706
|
||||
@@ -77,6 +82,162 @@ class SpelCompilerTests {
|
||||
assertThat(expression.getValue(context)).asInstanceOf(BOOLEAN).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleEvaluationContextBlocksCompilationByDefault() {
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
|
||||
SpelExpressionParser parser = new SpelExpressionParser(config);
|
||||
// "order" resides in the public Ordered interface and is therefore compilable,
|
||||
// so any non-compilation is attributable solely to the context's policy.
|
||||
Expression expression = parser.parseExpression("order");
|
||||
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
assertThat(context.isCompilationSupported()).isFalse();
|
||||
|
||||
// Evaluate the expression multiple times to ensure that it stays in interpreted mode,
|
||||
// effectively overriding SpelCompilerMode.IMMEDIATE.
|
||||
OrderedComponent component = new OrderedComponent();
|
||||
IntStream.rangeClosed(1, 5).forEach(i -> assertThat(expression.getValue(context, component)).isEqualTo(42));
|
||||
assertIsNotCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleEvaluationContextAllowsCompilationWhenSupported() {
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
|
||||
SpelExpressionParser parser = new SpelExpressionParser(config);
|
||||
// "order" resides in the public Ordered interface and is therefore compilable.
|
||||
Expression expression = parser.parseExpression("order");
|
||||
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding()
|
||||
.withCompilationSupported()
|
||||
.build();
|
||||
assertThat(context.isCompilationSupported()).isTrue();
|
||||
|
||||
// Two evaluations are enough for IMMEDIATE mode to compile.
|
||||
OrderedComponent component = new OrderedComponent();
|
||||
IntStream.rangeClosed(1, 2).forEach(i -> assertThat(expression.getValue(context, component)).isEqualTo(42));
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleEvaluationContextIgnoresPrecompiledExpressionByDefault() {
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
|
||||
SpelExpressionParser parser = new SpelExpressionParser(config);
|
||||
// "order" resides in the public Ordered interface and is therefore compilable.
|
||||
Expression expression = parser.parseExpression("order");
|
||||
|
||||
EvaluationContext standardContext = new StandardEvaluationContext();
|
||||
assertThat(standardContext.isCompilationSupported()).isTrue();
|
||||
OrderedComponent component = new OrderedComponent();
|
||||
IntStream.rangeClosed(1, 2).forEach(i -> assertThat(expression.getValue(standardContext, component)).isEqualTo(42));
|
||||
assertIsCompiled(expression);
|
||||
|
||||
// Switch to a SimpleEvaluationContext without opting into compilation — should
|
||||
// fall back to interpreted evaluation even though compiledAst is non-null.
|
||||
EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
assertThat(simpleContext.isCompilationSupported()).isFalse();
|
||||
|
||||
// Record interpretedCount before the simpleContext evaluation.
|
||||
// checkCompile() — which increments interpretedCount as its very first action —
|
||||
// is only reachable from the interpreted path. If the compiled path were taken
|
||||
// instead, interpretedCount would not change.
|
||||
int interpretedCountBefore = getInterpretedCount(expression);
|
||||
assertThat(expression.getValue(simpleContext, component)).isEqualTo(42);
|
||||
assertThat(getInterpretedCount(expression)).isEqualTo(interpretedCountBefore + 1);
|
||||
|
||||
// compiledAst is still set: the compiled expression was not cleared, rather merely ignored.
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the four implicit {@link EvaluationContext} {@code getValue()} variants
|
||||
* in {@link SpelExpression} honor a {@link SimpleEvaluationContext} set as the default
|
||||
* context: compilation must be blocked even under {@link SpelCompilerMode#IMMEDIATE}.
|
||||
*/
|
||||
@Test
|
||||
void simpleEvaluationContextSetAsDefaultBlocksCompilationForImplicitContextVariants() {
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
|
||||
SpelExpressionParser parser = new SpelExpressionParser(config);
|
||||
// "order" resides in the public Ordered interface and is therefore compilable,
|
||||
// so any non-compilation is attributable solely to the context's policy.
|
||||
SpelExpression expression = parser.parseRaw("order");
|
||||
|
||||
OrderedComponent component = new OrderedComponent();
|
||||
SimpleEvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding()
|
||||
.withRootObject(component)
|
||||
.build();
|
||||
assertThat(simpleContext.isCompilationSupported()).isFalse();
|
||||
expression.setEvaluationContext(simpleContext);
|
||||
|
||||
// Evaluate the expression multiple times using all four implicit context
|
||||
// variants to ensure that they stay in interpreted mode.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
assertThat(expression.getValue()).isEqualTo(42);
|
||||
assertIsNotCompiled(expression);
|
||||
|
||||
assertThat(expression.getValue(Integer.class)).isEqualTo(42);
|
||||
assertIsNotCompiled(expression);
|
||||
|
||||
assertThat(expression.getValue(component)).isEqualTo(42);
|
||||
assertIsNotCompiled(expression);
|
||||
|
||||
assertThat(expression.getValue(component, Integer.class)).isEqualTo(42);
|
||||
assertIsNotCompiled(expression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the four implicit {@link EvaluationContext} {@code getValue()} variants
|
||||
* in {@link SpelExpression} ignore a previously compiled expression when the default
|
||||
* context is a {@link SimpleEvaluationContext} (where {@code isCompilationSupported()}
|
||||
* returns {@code false}).
|
||||
*/
|
||||
@Test
|
||||
void simpleEvaluationContextSetAsDefaultIgnoresPrecompiledExpressionForImplicitContextVariants() {
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
|
||||
SpelExpressionParser parser = new SpelExpressionParser(config);
|
||||
// "order" resides in the public Ordered interface and is therefore compilable.
|
||||
SpelExpression expression = parser.parseRaw("order");
|
||||
|
||||
// Compile the expression via StandardEvaluationContext.
|
||||
StandardEvaluationContext standardContext = new StandardEvaluationContext();
|
||||
assertThat(standardContext.isCompilationSupported()).isTrue();
|
||||
OrderedComponent component = new OrderedComponent();
|
||||
IntStream.rangeClosed(1, 2).forEach(i ->
|
||||
assertThat(expression.getValue(standardContext, component, Integer.class)).isEqualTo(42));
|
||||
assertIsCompiled(expression);
|
||||
|
||||
// Switch to a SimpleEvaluationContext set as the default context — the precompiled
|
||||
// expression should be ignored for all four implicit context getValue() variants.
|
||||
SimpleEvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding()
|
||||
.withRootObject(component)
|
||||
.build();
|
||||
assertThat(simpleContext.isCompilationSupported()).isFalse();
|
||||
expression.setEvaluationContext(simpleContext);
|
||||
|
||||
// Record interpretedCount before the simpleContext evaluations.
|
||||
// checkCompile() — which increments interpretedCount as its very first action —
|
||||
// is only reachable from the interpreted path. If the compiled path were taken
|
||||
// instead, interpretedCount would not change.
|
||||
int interpretedCountBefore = getInterpretedCount(expression);
|
||||
assertThat(expression.getValue()).isEqualTo(42);
|
||||
assertThat(getInterpretedCount(expression)).isEqualTo(interpretedCountBefore + 1);
|
||||
|
||||
interpretedCountBefore = getInterpretedCount(expression);
|
||||
assertThat(expression.getValue(Integer.class)).isEqualTo(42);
|
||||
assertThat(getInterpretedCount(expression)).isEqualTo(interpretedCountBefore + 1);
|
||||
|
||||
interpretedCountBefore = getInterpretedCount(expression);
|
||||
assertThat(expression.getValue(component)).isEqualTo(42);
|
||||
assertThat(getInterpretedCount(expression)).isEqualTo(interpretedCountBefore + 1);
|
||||
|
||||
interpretedCountBefore = getInterpretedCount(expression);
|
||||
assertThat(expression.getValue(component, Integer.class)).isEqualTo(42);
|
||||
assertThat(getInterpretedCount(expression)).isEqualTo(interpretedCountBefore + 1);
|
||||
|
||||
// compiledAst is still set: the compiled expression was not cleared, rather merely ignored.
|
||||
assertIsCompiled(expression);
|
||||
}
|
||||
|
||||
@Test // gh-28043
|
||||
void changingRegisteredVariableTypeDoesNotResultInFailureInMixedMode() {
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.MIXED, null);
|
||||
|
||||
+31
@@ -17,6 +17,7 @@
|
||||
package org.springframework.expression.spel.standard;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
|
||||
@@ -26,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests utilities for {@link SpelExpression}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public abstract class SpelExpressionTestUtils {
|
||||
|
||||
@@ -41,4 +43,33 @@ public abstract class SpelExpressionTestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertIsNotCompiled(Expression expression) {
|
||||
try {
|
||||
Field field = SpelExpression.class.getDeclaredField("compiledAst");
|
||||
field.setAccessible(true);
|
||||
Object object = field.get(expression);
|
||||
assertThat(object).isNull();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current interpreted evaluation count for the given expression.
|
||||
* <p>This counter is incremented exclusively by the interpreted evaluation path
|
||||
* (inside {@code checkCompile()}), so it serves as a reliable witness for
|
||||
* distinguishing interpreted from compiled evaluations in tests.
|
||||
*/
|
||||
public static int getInterpretedCount(Expression expression) {
|
||||
try {
|
||||
Field field = SpelExpression.class.getDeclaredField("interpretedCount");
|
||||
field.setAccessible(true);
|
||||
return ((AtomicInteger) field.get(expression)).get();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
@@ -53,6 +53,24 @@ class SimpleEvaluationContextTests {
|
||||
private final Model model = new Model();
|
||||
|
||||
|
||||
@Test
|
||||
void compilationNotSupportedByDefault() {
|
||||
assertThat(SimpleEvaluationContext.forReadOnlyDataBinding().build().isCompilationSupported()).isFalse();
|
||||
assertThat(SimpleEvaluationContext.forReadWriteDataBinding().build().isCompilationSupported()).isFalse();
|
||||
assertThat(SimpleEvaluationContext.forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess())
|
||||
.build().isCompilationSupported()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void compilationSupportedViaBuilder() {
|
||||
assertThat(SimpleEvaluationContext.forReadOnlyDataBinding()
|
||||
.withCompilationSupported().build().isCompilationSupported()).isTrue();
|
||||
assertThat(SimpleEvaluationContext.forReadWriteDataBinding()
|
||||
.withCompilationSupported().build().isCompilationSupported()).isTrue();
|
||||
assertThat(SimpleEvaluationContext.forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess())
|
||||
.withCompilationSupported().build().isCompilationSupported()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void forReadWriteDataBinding() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding()
|
||||
|
||||
Reference in New Issue
Block a user