diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index 0f7ef0f7927..005903ca0ee 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -185,4 +185,26 @@ public interface EvaluationContext { return true; } + /** + * Determine if compilation is supported within expressions evaluated by this evaluation + * context. + *
This flag is orthogonal to whether compilation has been enabled via + * {@link org.springframework.expression.spel.SpelParserConfiguration} or the + * {@value org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME} + * system property. Both must permit compilation for a given expression to be compiled. + *
If this method returns {@code false}, the SpEL compiler will not compile + * expressions evaluated within this context, regardless of the compiler mode + * configured in the associated {@code SpelParserConfiguration} or system property. + * Furthermore, if a compiled form of the expression already exists, it will not be + * used; instead, interpreted evaluation will be performed. + *
By default, this method returns {@code true}. Concrete implementations may override
+ * this default method to indicate that compilation is not supported.
+ * @return {@code true} if compilation is supported; {@code false} otherwise
+ * @since 7.0.9
+ * @see org.springframework.expression.spel.support.SimpleEvaluationContext.Builder#withCompilationSupported()
+ */
+ default boolean isCompilationSupported() {
+ return true;
+ }
+
}
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java
index 87133da0779..6664f17f358 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java
@@ -75,14 +75,14 @@ public class SpelExpression implements Expression {
private final SpelParserConfiguration configuration;
- // The default context is used if no override is supplied by the user
+ // The default context is used if no override is supplied by the user.
private @Nullable EvaluationContext evaluationContext;
- // Holds the compiled form of the expression (if it has been compiled)
+ // Holds the compiled form of the expression (if it has been compiled).
private volatile @Nullable CompiledExpression compiledAst;
- // Count of many times as the expression been interpreted - can trigger compilation
- // when certain limit reached
+ // Counts how many times the expression has been interpreted - can trigger compilation
+ // when a certain limit is reached.
private final AtomicInteger interpretedCount = new AtomicInteger();
// The number of times compilation was attempted and failed - enables us to eventually
@@ -129,10 +129,10 @@ public class SpelExpression implements Expression {
@Override
public @Nullable Object getValue() throws EvaluationException {
+ EvaluationContext context = getEvaluationContext();
CompiledExpression compiledAst = this.compiledAst;
- if (compiledAst != null) {
+ if (compiledAst != null && context.isCompilationSupported()) {
try {
- EvaluationContext context = getEvaluationContext();
return compiledAst.getValue(context.getRootObject().getValue(), context);
}
catch (Throwable ex) {
@@ -148,7 +148,7 @@ public class SpelExpression implements Expression {
}
}
- ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration);
+ ExpressionState expressionState = new ExpressionState(context, this.configuration);
Object result = this.ast.getValue(expressionState);
checkCompile(expressionState);
return result;
@@ -157,17 +157,16 @@ public class SpelExpression implements Expression {
@SuppressWarnings("unchecked")
@Override
public By default, compilation is not supported in {@code SimpleEvaluationContext}.
+ * @return {@code true} if compilation is supported; {@code false} otherwise
+ * @since 7.0.9
+ * @see Builder#withCompilationSupported()
+ */
+ @Override
+ public boolean isCompilationSupported() {
+ return this.compilationSupported;
+ }
+
/**
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
* delegates: typically a custom {@code PropertyAccessor} specific to a use case —
@@ -375,6 +391,8 @@ public final class SimpleEvaluationContext implements EvaluationContext {
private boolean assignmentEnabled = true;
+ private boolean compilationSupported = false;
+
private Builder(PropertyAccessor... accessors) {
this.propertyAccessors = Arrays.asList(accessors);
@@ -391,6 +409,22 @@ public final class SimpleEvaluationContext implements EvaluationContext {
return this;
}
+ /**
+ * Indicate that compilation is supported within expressions evaluated by this
+ * evaluation context.
+ * By default, compilation is not supported in {@code SimpleEvaluationContext}.
+ * Call this method to opt in to compilation — for example, when evaluating
+ * trusted expressions where performance is critical.
+ * WARNING: Opting in to compilation for expressions from
+ * untrusted sources removes the safety guards supported in interpreted mode.
+ * @since 7.0.9
+ * @see SimpleEvaluationContext#isCompilationSupported()
+ */
+ public Builder withCompilationSupported() {
+ this.compilationSupported = true;
+ return this;
+ }
+
/**
* Register the specified {@link IndexAccessor} delegates.
* @param indexAccessors the index accessors to use
@@ -480,7 +514,8 @@ public final class SimpleEvaluationContext implements EvaluationContext {
public SimpleEvaluationContext build() {
return new SimpleEvaluationContext(this.propertyAccessors, this.indexAccessors,
- this.resolvers, this.typeConverter, this.rootObject, this.assignmentEnabled);
+ this.resolvers, this.typeConverter, this.rootObject,
+ this.assignmentEnabled, this.compilationSupported);
}
}
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java
index 65f957d4a2a..67799dd7ab5 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java
@@ -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);
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelExpressionTestUtils.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelExpressionTestUtils.java
index 124071379e3..2e55822f934 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelExpressionTestUtils.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelExpressionTestUtils.java
@@ -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.
+ * 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);
+ }
+ }
+
}
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java
index f3e5a2cfa3a..79f04df0c0f 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java
@@ -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()