diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 4d0e8ad291c..f59a59e769a 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -2,7 +2,7 @@ name: Build and Deploy Snapshot on: push: branches: - - main + - 'main' concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index 8c2cbd703ff..9107fe9c5a2 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -74,6 +74,12 @@ expressions used in XML bean definitions, `@Value`, etc. | The mode to use when compiling expressions for the xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language]. +| `spring.expression.maxBigPowerBits` +| The default maximum number of bits permitted in the result of a `BigDecimal` or +`BigInteger` power operation within a +xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language] +expression. + | `spring.expression.maxOperations` | The default maximum number of operations permitted during xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language] diff --git a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc index cb046c10a79..ef3668351bb 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -574,6 +574,19 @@ property or Spring property named `spring.expression.maxOperations` to the maxim of operations required by your application (see xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]). +In addition, the result of a `BigDecimal` or `BigInteger` power operation within a SpEL +expression cannot exceed 1,000,000 bits by default – approximately equivalent to a +decimal number with 300,000 digits. Power operations involving large base values or large +exponents can be computationally expensive, and this limit ensures that evaluations +remain bounded; however, the `maximumBigPowerBits` value is configurable. If you create a +`SpelExpressionParser` programmatically (the recommended approach), you can specify a +custom `maximumBigPowerBits` value when creating the `SpelParserConfiguration` that you +provide to the `SpelExpressionParser`. To remove this limit entirely, pass +`Integer.MAX_VALUE` as the `maximumBigPowerBits` value. If you are not able to configure +an explicit value for `maximumBigPowerBits` via `SpelParserConfiguration`, you can set a +JVM system property or Spring property named `spring.expression.maxBigPowerBits` to the +maximum result size in bits (see xref:appendix.adoc#appendix-spring-properties[Supported +Spring Properties]). [[expressions-spel-compilation]] == SpEL Compilation diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8a72bad61c3..38bc2078027 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -8,9 +8,9 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.21.5")) - api(platform("io.micrometer:micrometer-bom:1.18.0-SNAPSHOT")) + api(platform("io.micrometer:micrometer-bom:1.18.0-M1")) api(platform("io.netty:netty-bom:4.2.17.Final")) - api(platform("io.projectreactor:reactor-bom:2026.0.0-SNAPSHOT")) + api(platform("io.projectreactor:reactor-bom:2026.0.0-M1")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:5.0.8")) api(platform("org.apache.logging.log4j:log4j-bom:2.26.1")) diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 8253a9fe9cd..4c763b480f5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -632,6 +632,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA else if (value instanceof List list) { int index = Integer.parseInt(key); growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); + if (index < 0 || index >= list.size()) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot get element with index " + index + " from List of size " + + list.size() + ", accessed using property path '" + propertyName + "'"); + } value = list.get(index); } else if (value instanceof Map map) { diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java index 2b3480404fa..33194ed8400 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java @@ -16,6 +16,7 @@ package org.springframework.beans; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -183,7 +184,34 @@ class BeanWrapperAutoGrowingTests { wrapper.setAutoGrowCollectionLimit(2); assertThatExceptionOfType(InvalidPropertyException.class) .isThrownBy(() -> wrapper.getPropertyValue("list[4]")) - .withRootCauseInstanceOf(IndexOutOfBoundsException.class); + .withMessageContainingAll( + "Invalid property 'list[4]'", + "Cannot get element with index 4 from List of size 0"); + } + + @Test + void getPropertyValueSelfPopulatingListWorksWithinLimit() { + bean.setList(new SelfPopulatingList()); + assertThat(wrapper.getPropertyValue("list[2]")).isInstanceOf(Bean.class); + assertThat(bean.getList()) + .hasSize(3) + .allSatisfy(entry -> assertThat(entry).isInstanceOf(Bean.class)); + } + + @Test + void getPropertyValueSelfPopulatingListFailsAgainstLimit() { + bean.setList(new SelfPopulatingList()); + wrapper.setAutoGrowCollectionLimit(2); + assertThatExceptionOfType(InvalidPropertyException.class) + .isThrownBy(() -> wrapper.getPropertyValue("list[4]")); + } + + @Test + void setPropertyValueSelfPopulatingListFailsAgainstLimitForNestedPath() { + bean.setList(new SelfPopulatingList()); + wrapper.setAutoGrowCollectionLimit(2); + assertThatExceptionOfType(InvalidPropertyException.class) + .isThrownBy(() -> wrapper.setPropertyValue("list[4].prop", "test")); } @Test @@ -382,4 +410,24 @@ class BeanWrapperAutoGrowingTests { } } + + /** + * A {@link List} implementation that creates elements on demand in {@link #get(int)} + * instead of throwing {@link IndexOutOfBoundsException} for out-of-range indexes. + * + *
Used to verify that {@link BeanWrapperImpl} does not delegate to
+ * {@link List#get(int)} for indexes beyond the configured auto-grow limit.
+ */
+ @SuppressWarnings("serial")
+ private static class SelfPopulatingList extends ArrayList 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/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java
index d58c89e7aea..28cea823c73 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java
@@ -307,7 +307,12 @@ public enum SpelMessage {
/** @since 6.2.19 */
MAX_OPERATIONS_EXCEEDED(Kind.ERROR, 1085,
- "SpEL expression evaluation exceeded the threshold of ''{0}'' operations");
+ "SpEL expression evaluation exceeded the threshold of ''{0}'' operations"),
+
+ /** @since 7.0.9 */
+ MAX_BIG_POWER_RESULT_EXCEEDED(Kind.ERROR, 1086,
+ "BigDecimal/BigInteger power operation with base bit length ''{0}'' and exponent ''{1}'' " +
+ "would produce a result exceeding the configured maximum of ''{2}'' bits");
private final Kind kind;
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java
index aa2a3db8c74..73e218ec5f8 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java
@@ -49,6 +49,16 @@ public class SpelParserConfiguration {
*/
public static final int DEFAULT_MAX_OPERATIONS = 10_000;
+ /**
+ * Default maximum number of bits permitted in the result of a
+ * {@link java.math.BigDecimal} or {@link java.math.BigInteger} power operation
+ * within a SpEL expression: {@value}.
+ * Approximately equivalent to a decimal number with 300,000 digits.
+ * @since 7.0.9
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
+ */
+ public static final int DEFAULT_MAX_BIG_POWER_BITS = 1_000_000;
+
/**
* System property to configure the default compiler mode for SpEL expression parsers: {@value}.
* NOTE: Instead of relying on a global default, applications
@@ -65,7 +75,7 @@ public class SpelParserConfiguration {
* during SpEL expression evaluation: {@value}.
* NOTE: Instead of relying on a global default, applications
* and frameworks should ideally set an explicit custom value via the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* Can also be configured via the {@link SpringProperties} mechanism.
@@ -74,6 +84,22 @@ public class SpelParserConfiguration {
*/
public static final String SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME = "spring.expression.maxOperations";
+ /**
+ * System property to configure the default maximum number of bits permitted in the
+ * result of a {@link java.math.BigDecimal} or {@link java.math.BigInteger} power
+ * operation within a SpEL expression: {@value}.
+ * NOTE: Instead of relying on a global default, applications
+ * and frameworks should ideally set an explicit custom value via the
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
+ * constructor which provides complete configuration control and the ability
+ * to override global defaults per use case.
+ * Can also be configured via the {@link SpringProperties} mechanism.
+ * @since 7.0.9
+ * @see #DEFAULT_MAX_BIG_POWER_BITS
+ */
+ public static final String SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME =
+ "spring.expression.maxBigPowerBits";
+
private static final SpelCompilerMode defaultCompilerMode;
@@ -98,15 +124,18 @@ public class SpelParserConfiguration {
private final int maximumOperations;
+ private final int maximumBigPowerBits;
+
/**
* Create a new {@code SpelParserConfiguration} instance with default settings.
* NOTE: Favor the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
*/
public SpelParserConfiguration() {
this(null, null, false, false, Integer.MAX_VALUE);
@@ -115,7 +144,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* NOTE: Favor the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -124,6 +153,7 @@ public class SpelParserConfiguration {
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader) {
this(compilerMode, compilerClassLoader, false, false, Integer.MAX_VALUE);
@@ -132,13 +162,14 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* NOTE: Favor the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
*/
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) {
this(null, null, autoGrowNullReferences, autoGrowCollections, Integer.MAX_VALUE);
@@ -147,7 +178,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* NOTE: Favor the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
@@ -155,6 +186,7 @@ public class SpelParserConfiguration {
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
*/
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
this(null, null, autoGrowNullReferences, autoGrowCollections, maximumAutoGrowSize);
@@ -163,7 +195,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* NOTE: Favor the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -175,6 +207,7 @@ public class SpelParserConfiguration {
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
@@ -186,7 +219,7 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* NOTE: Favor the
- * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -201,6 +234,7 @@ public class SpelParserConfiguration {
* @since 5.2.25
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) {
@@ -209,6 +243,34 @@ public class SpelParserConfiguration {
autoGrowCollections, maximumAutoGrowSize, maximumExpressionLength, retrieveMaxOperations());
}
+ /**
+ * Create a new {@code SpelParserConfiguration} instance.
+ * NOTE: Favor the
+ * {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int, int)}
+ * constructor for complete configuration control and the ability to override
+ * global defaults per use case.
+ * @param compilerMode the compiler mode that parsers using this configuration
+ * should use; must not be {@code null}
+ * @param compilerClassLoader the {@code ClassLoader} to use as the basis for
+ * expression compilation; or {@code null} to use the default {@code ClassLoader}
+ * @param autoGrowNullReferences if null references should automatically grow
+ * @param autoGrowCollections if collections should automatically grow
+ * @param maximumAutoGrowSize the maximum size to which a collection can auto grow
+ * @param maximumExpressionLength the maximum length of a SpEL expression;
+ * must be a positive number
+ * @param maximumOperations the maximum number of operations permitted during
+ * SpEL expression evaluation; must be a positive number
+ * @since 6.2.19
+ * @see #SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
+ */
+ public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
+ boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
+ int maximumOperations) {
+
+ this(compilerMode, compilerClassLoader, autoGrowNullReferences, autoGrowCollections,
+ maximumAutoGrowSize, maximumExpressionLength, maximumOperations, retrieveMaxBigPowerBits());
+ }
+
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration
@@ -222,15 +284,19 @@ public class SpelParserConfiguration {
* must be a positive number
* @param maximumOperations the maximum number of operations permitted during
* SpEL expression evaluation; must be a positive number
- * @since 6.2.19
+ * @param maximumBigPowerBits the maximum number of bits permitted in the
+ * result of a {@link java.math.BigDecimal} or {@link java.math.BigInteger} power
+ * operation; must be a positive number; use {@link Integer#MAX_VALUE} for no limit
+ * @since 7.0.9
*/
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
- int maximumOperations) {
+ int maximumOperations, int maximumBigPowerBits) {
Assert.notNull(compilerMode, "'compilerMode' must not be null");
Assert.isTrue(maximumExpressionLength > 0, "'maximumExpressionLength' must be a positive number");
Assert.isTrue(maximumOperations > 0, "'maximumOperations' must be a positive number");
+ Assert.isTrue(maximumBigPowerBits > 0, "'maximumBigPowerBits' must be a positive number");
this.compilerMode = compilerMode;
this.compilerClassLoader = compilerClassLoader;
@@ -239,6 +305,7 @@ public class SpelParserConfiguration {
this.maximumAutoGrowSize = maximumAutoGrowSize;
this.maximumExpressionLength = maximumExpressionLength;
this.maximumOperations = maximumOperations;
+ this.maximumBigPowerBits = maximumBigPowerBits;
}
@@ -294,6 +361,15 @@ public class SpelParserConfiguration {
return this.maximumOperations;
}
+ /**
+ * Return the maximum number of bits permitted in the result of a
+ * {@link java.math.BigDecimal} or {@link java.math.BigInteger} power operation.
+ * @since 7.0.9
+ */
+ public int getMaximumBigPowerBits() {
+ return this.maximumBigPowerBits;
+ }
+
private static int retrieveMaxOperations() {
String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME);
@@ -313,4 +389,21 @@ public class SpelParserConfiguration {
}
}
+ private static int retrieveMaxBigPowerBits() {
+ String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME);
+ if (!StringUtils.hasText(value)) {
+ return DEFAULT_MAX_BIG_POWER_BITS;
+ }
+ try {
+ int maxBits = Integer.parseInt(value.trim());
+ Assert.isTrue(maxBits > 0, () -> "Value [" + maxBits + "] for system property [" +
+ SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME + "] must be positive");
+ return maxBits;
+ }
+ catch (NumberFormatException ex) {
+ throw new IllegalArgumentException("Failed to parse value for system property [" +
+ SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME + "]: " + ex.getMessage(), ex);
+ }
+ }
+
}
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java
index 1891db890f6..ac7e4eeadce 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java
@@ -23,13 +23,15 @@ import org.springframework.expression.EvaluationException;
import org.springframework.expression.Operation;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.ExpressionState;
-import org.springframework.util.NumberUtils;
+import org.springframework.expression.spel.SpelEvaluationException;
+import org.springframework.expression.spel.SpelMessage;
/**
* The power operator.
*
* @author Andy Clement
* @author Giovanni Dall'Oglio Risso
+ * @author Sam Brannen
* @since 3.0
*/
public class OperatorPower extends Operator {
@@ -41,21 +43,20 @@ public class OperatorPower extends Operator {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
- SpelNodeImpl leftOp = getLeftOperand();
- SpelNodeImpl rightOp = getRightOperand();
-
- Object leftOperand = leftOp.getValueInternal(state).getValue();
- Object rightOperand = rightOp.getValueInternal(state).getValue();
+ Object leftOperand = getLeftOperand().getValueInternal(state).getValue();
+ Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
- if (leftNumber instanceof BigDecimal) {
- BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
- return new TypedValue(leftBigDecimal.pow(rightNumber.intValue()));
+ if (leftNumber instanceof BigDecimal leftBigDecimal) {
+ int exponent = rightNumber.intValue();
+ checkBigNumberPowerBits(state, leftBigDecimal.unscaledValue().bitLength(), exponent);
+ return new TypedValue(leftBigDecimal.pow(exponent));
}
- else if (leftNumber instanceof BigInteger) {
- BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class);
- return new TypedValue(leftBigInteger.pow(rightNumber.intValue()));
+ else if (leftNumber instanceof BigInteger leftBigInteger) {
+ int exponent = rightNumber.intValue();
+ checkBigNumberPowerBits(state, leftBigInteger.bitLength(), exponent);
+ return new TypedValue(leftBigInteger.pow(exponent));
}
else if (leftNumber instanceof Double || rightNumber instanceof Double) {
return new TypedValue(Math.pow(leftNumber.doubleValue(), rightNumber.doubleValue()));
@@ -76,4 +77,13 @@ public class OperatorPower extends Operator {
return state.operate(Operation.POWER, leftOperand, rightOperand);
}
+ private void checkBigNumberPowerBits(ExpressionState state, int baseBitLength, int exponent) {
+ int maxBigPowerBits = state.getConfiguration().getMaximumBigPowerBits();
+ long estimatedBigPowerBits = (long) baseBitLength * exponent;
+ if (estimatedBigPowerBits > maxBigPowerBits) {
+ throw new SpelEvaluationException(getStartPosition(), SpelMessage.MAX_BIG_POWER_RESULT_EXCEEDED,
+ baseBitLength, exponent, maxBigPowerBits);
+ }
+ }
+
}
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/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java
index 31a248a08b7..721ed9fd81e 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java
@@ -19,6 +19,7 @@ package org.springframework.expression.spel;
import java.util.Arrays;
import java.util.List;
+import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -168,6 +169,23 @@ public abstract class AbstractExpressionTests {
evaluateAndCheckError(this.parser, expression, expectedReturnType, expectedMessage, otherProperties);
}
+ /**
+ * Evaluate the specified expression and ensure the expected message comes out.
+ * The message may have inserts and they will be checked if otherProperties is specified.
+ * The first entry in otherProperties should always be the position.
+ * @param evaluationContext the evaluation context to use
+ * @param expression the expression to evaluate
+ * @param expectedReturnType ask the expression return value to be of this type if possible
+ * ({@code null} indicates don't ask for conversion)
+ * @param expectedMessage the expected message
+ * @param otherProperties the expected inserts within the message
+ */
+ protected void evaluateAndCheckError(EvaluationContext evaluationContext, String expression,
+ Class> expectedReturnType, SpelMessage expectedMessage, Object... otherProperties) {
+
+ evaluateAndCheckError(this.parser, evaluationContext, expression, expectedReturnType, expectedMessage, otherProperties);
+ }
+
/**
* Evaluate the specified expression and ensure the expected message comes out.
* The message may have inserts and they will be checked if otherProperties is specified.
@@ -182,14 +200,32 @@ public abstract class AbstractExpressionTests {
protected void evaluateAndCheckError(ExpressionParser parser, String expression, Class> expectedReturnType, SpelMessage expectedMessage,
Object... otherProperties) {
+ evaluateAndCheckError(parser, this.context, expression, expectedReturnType, expectedMessage, otherProperties);
+ }
+
+ /**
+ * Evaluate the specified expression and ensure the expected message comes out.
+ * The message may have inserts and they will be checked if otherProperties is specified.
+ * The first entry in otherProperties should always be the position.
+ * @param parser the expression parser to use
+ * @param evaluationContext the evaluation context to use
+ * @param expression the expression to evaluate
+ * @param expectedReturnType ask the expression return value to be of this type if possible
+ * ({@code null} indicates don't ask for conversion)
+ * @param expectedMessage the expected message
+ * @param otherProperties the expected inserts within the message
+ */
+ protected void evaluateAndCheckError(ExpressionParser parser, EvaluationContext evaluationContext,
+ String expression, Class> expectedReturnType, SpelMessage expectedMessage, Object... otherProperties) {
+
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> {
Expression expr = parser.parseExpression(expression);
assertThat(expr).as("expression").isNotNull();
if (expectedReturnType != null) {
- expr.getValue(context, expectedReturnType);
+ expr.getValue(evaluationContext, expectedReturnType);
}
else {
- expr.getValue(context);
+ expr.getValue(evaluationContext);
}
}).satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(expectedMessage);
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java
index 06290373568..494cd7856ac 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java
@@ -811,6 +811,77 @@ class EvaluationTests extends AbstractExpressionTests {
}
+ @Nested
+ class PowerOperatorTests {
+
+ private final EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
+
+ // Use a small limit (16 bits) to verify behavior in tests.
+ private static final int TEST_MAX_RESULT_BITS = 16;
+
+ private final SpelExpressionParser limitedParser = new SpelExpressionParser(
+ new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false,
+ 0, 10, 10, TEST_MAX_RESULT_BITS));
+
+
+ @Test
+ void powerOperatorWithBigDecimal() {
+ context.setVariable("bd", BigDecimal.valueOf(2.0));
+ Expression expr = parser.parseExpression("#bd ^ 4");
+ assertThat(expr.getValue(context, BigDecimal.class)).isEqualByComparingTo("16");
+ }
+
+ @Test
+ void powerOperatorWithBigDecimalUnderResultLimit() {
+ // BigDecimal.valueOf(2.0).unscaledValue().bitLength() = 5
+ // 5 * 3 = 15 bits <= TEST_MAX_RESULT_BITS (16)
+ context.setVariable("bd", BigDecimal.valueOf(2.0));
+ Expression expr = limitedParser.parseExpression("#bd ^ 3");
+ assertThat(expr.getValue(context, BigDecimal.class)).isEqualByComparingTo("8");
+ }
+
+ @Test
+ void powerOperatorWithBigDecimalExceedingResultLimit() {
+ // 5 * 4 = 20 bits > TEST_MAX_RESULT_BITS (16)
+ context.setVariable("bd", BigDecimal.valueOf(2.0));
+ evaluateAndCheckError(limitedParser, context, "#bd ^ 4", BigDecimal.class,
+ SpelMessage.MAX_BIG_POWER_RESULT_EXCEEDED,
+ 4, // power operator position
+ 5, // base bit length
+ 4, // exponent
+ TEST_MAX_RESULT_BITS);
+ }
+
+ @Test
+ void powerOperatorWithBigInteger() {
+ context.setVariable("bi", BigInteger.valueOf(2));
+ Expression expr = parser.parseExpression("#bi ^ 4");
+ assertThat(expr.getValue(context, BigInteger.class)).isEqualTo(BigInteger.valueOf(16));
+ }
+
+ @Test
+ void powerOperatorWithBigIntegerUnderResultLimit() {
+ // BigInteger.valueOf(2).bitLength() = 2
+ // 2 * 8 = 16 bits == TEST_MAX_RESULT_BITS (16)
+ context.setVariable("bi", BigInteger.valueOf(2));
+ Expression expr = limitedParser.parseExpression("#bi ^ 8");
+ assertThat(expr.getValue(context, BigInteger.class)).isEqualTo(BigInteger.valueOf(256));
+ }
+
+ @Test
+ void powerOperatorWithBigIntegerExceedingResultLimit() {
+ // 2 * 9 = 18 bits > TEST_MAX_RESULT_BITS (16)
+ context.setVariable("bi", BigInteger.valueOf(2));
+ evaluateAndCheckError(limitedParser, context, "#bi ^ 9", BigInteger.class,
+ SpelMessage.MAX_BIG_POWER_RESULT_EXCEEDED,
+ 4, // power operator position
+ 2, // base bit length
+ 9, // exponent
+ TEST_MAX_RESULT_BITS);
+ }
+
+ }
+
@Nested
class TernaryOperatorTests {
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()
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java
index f2f1da621dd..19a6eb2f68b 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java
@@ -103,8 +103,9 @@ class MessagingRSocket implements RSocket {
* @return completion handle for success or error
*/
public Mono> split(Flux
> split(
+ Flux